Use ExpandoObject to create a 'fake' implementation of an interface - adding methods dynamically

422 views Asked by At

A brainteaser for you!

I am developing a modular system, in such a way that module A could need module B and module B could also need module A. But if module B is disabled, it will simply not execute that code and do nothing / return null.

A little bit more into perspective:

Let's say InvoiceBusinessLogic is within module "Core". We also have a "Ecommerce" module which has a OrderBusinessLogic. The InvoiceBusinessLogic could then look like this:

public class InvoiceBusinessLogic : IInvoiceBusinessLogic
{
    private readonly IOrderBusinessLogic _orderBusinessLogic;

    public InvoiceBusinessLogic(IOrderBusinessLogic orderBusinessLogic)
    {
        _orderBusinessLogic = orderBusinessLogic;
    }

    public void UpdateInvoicePaymentStatus(InvoiceModel invoice)
    {
        _orderBusinessLogic.UpdateOrderStatus(invoice.OrderId);
    }
}

So what I want is: When the module "Ecommerce" is enabled, it would actually do something at the OrderBusinessLogic. When not, it would simply not do anything. In this example it returns nothing so it can simply do nothing, in other examples where something would be returned, it would return null.

Notes:

  • As you can probably tell, I am using Dependency Injection, it is a ASP.NET Core application so the IServiceCollection takes care of defining the implementations.
  • Simply not defining the implementation for IOrderBusinessLogic will cause a runtime issue, logically.
  • From a lot of research done, I do not want to make calls to the container within my domain / logic of the app. Don't call the DI Container, it'll call you
  • These kind of interactions between modules are kept to a minimum, preferably done within the controller, but sometimes you cannot get around it (and also in the controller I would then need a way to inject them and use them or not).

So there are 3 options that I figured out so far:

  1. I never make calls from module "Core" to module "Ecommerce", in theory this sounds the best way, but in practice it's more complicated for advanced scenarios. Not an option
  2. I could create a lot of fake implementations, depending on the configuration decide on which one to implement. But that would of course result in double code and I would constantly have to update the fake class when a new method is introduced. So not perfectly.
  3. I can build up a fake implementation by using reflection and ExpandoObject, and just do nothing or return null when the particular method is called.

And the last option is what I am now after:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled)
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
        return;
    }
    dynamic expendo = new ExpandoObject();
    IOrderBusinessLogic fakeBusinessLogic = Impromptu.ActLike(expendo);
    services.AddTransient<IOrderBusinessLogic>(x => fakeBusinessLogic);
}

By using Impromptu Interface, I am able to successfully create a fake implementation. But what I now need to solve is that the dynamic object also contains all the methods (mostly properties not needed), but those ones are easy to add. So currently I am able to run the code and get up until the point it will call the OrderBusinessLogic, then it will, logically, throw an exception that the method does not exist.

By using reflection, I can iterate over all the methods within the interface, but how do I add them to the dynamic object?

dynamic expendo = new ExpandoObject();
var dictionary = (IDictionary<string, object>)expendo;
var methods = typeof(IOrderBusinessLogic).GetMethods(BindingFlags.Public);
foreach (MethodInfo method in methods)
{
    var parameters = method.GetParameters();
    //insert magic here
}

Note: For now directly calling typeof(IOrderBusinessLogic), but later I would iterate over all the interfaces within a certain assembly.

Impromptu has an example as follows: expando.Meth1 = Return<bool>.Arguments<int>(it => it > 5);

But of course I want this to be dynamic so how do I dynamically insert the return type and the parameters.

I do understand that a interface acts like a contract, and that contract should be followed, I also understand that this is an anti-pattern, but extensive research and negotiations have been done prior to reaching this point, for the result system we want, we think this is the best option, just a little missing piece :).

  • I have looked at this question, I am not really planning on leaving .dll's out, because most likely I would not be able to have any form of IOrderBusinessLogic usable within InvoiceBusinessLogic.
  • I have looked at this question, but I did not really understand how TypeBuilder could be used in my scenario
  • I have also looked into Mocking the interfaces, but mostly you would then need to define the 'mocking implementation' for each method that you want to change, correct me if I am wrong.
2

There are 2 answers

7
CularBytes On

For as long as there is no other answer for the solution I am looking for, I came up with the following extension:

using ImpromptuInterface.Build;
public static TInterface IsModuleEnabled<TInterface>(this TInterface obj) where TInterface : class
{
    if (obj is ActLikeProxy)
    {
        return default(TInterface);//returns null
    }
    return obj;
}

And then use it like:

public void UpdateInvoicePaymentStatus(InvoiceModel invoice)
{
    _orderBusinessLogic.IsModuleEnabled()?.UpdateOrderStatus(invoice.OrderId);
   //just example stuff
   int? orderId = _orderBusinessLogic.IsModuleEnabled()?.GetOrderIdForInvoiceId(invoice.InvoiceId);
}

And actually it has the advantage that it is clear (in the code) that the return type can be null or the method won't be called when the module is disabled. The only thing that should be documented carefully, or in another way enforced, that is has to be clear which classes do not belong to the current module. The only thing I could think of right now is by not including the using automatically, but use the full namespace or add summaries to the included _orderBusinessLogic, so when someone is using it, it is clear this belongs to another module, and a null check should be performed.

For those that are interested, here is the code to correctly add all fake implementations:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled) 
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
        return;
    }
    //just pick one interface in the correct assembly.
    var types = Assembly.GetAssembly(typeof(IOrderBusinessLogic)).GetExportedTypes();
    AddFakeImplementations(services, types);
}

using ImpromptuInterface;
private static void AddFakeImplementations(IServiceCollection services, Type[] types)
{
    //filtering on public interfaces and my folder structure / naming convention
    types = types.Where(x =>
        x.IsInterface && x.IsPublic &&
        (x.Namespace.Contains("BusinessLogic") || x.Namespace.Contains("Repositories"))).ToArray();
    foreach (Type type in types)
    {
        dynamic expendo = new ExpandoObject();
        var fakeImplementation = Impromptu.DynamicActLike(expendo, type);
        services.AddTransient(type, x => fakeImplementation);

    }
}
6
Spotted On

Even tough the third approach (with ExpandoObject) looks like a holy grail, I foster you to not follow this path for the following reasons:

  • What guarantees you that this fancy logic will be error-free now and at every time in the future ? (think: in 1 year you add a property in IOrderBusinessLogic)
  • What are the consequences if not ? Maybe an unexpected message will pop to the user or cause some strange "a priori unrelated" behavior

I would definitely go down the second option (fake implementation, also called Null-Object) even though, yes it will require to write some boilerplate code but ey this would offer you a compile-time guarantee that nothing unexpected will happen at rutime !

So my advice would be to do something like this:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled)
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
    }
    else
    {
        services.AddTransient<IOrderBusinessLogic, EmptyOrderBusinessLogic>();
    }
}