The chicken can dance

Rubber Chicken Paradise

Making the chicken dance one line of code at a time

Dynamic factories for Multi-Tenant systems in .net core - Hacking in Named Registrations and using configuration to get them

Reflection, Microsoft.Extensions.Configuration, Microsoft.Extensions.DependencyInjection, and a healthy dose of not stopping to ask if we should.

Jeremy Oursler

5 minutes read

When working in multi-tenant systems being able to swap implementations based on which client is logged in can be very helpful.

There may be custom work to a module for a specific client, or different implementations for different back-end services, or one of a thousand reasons you can probably think of where having a named instance would be useful.

Unfortunately Microsoft.Extensions.DependencyInjection does not have this out of the box.

There are several different ways I have seen this handled, relied on configuration but was a trivial use case here. Some used single named interfaces and registered those (of course thats not much better than just registering the actual class instead of the interface), some have used generic interfaces like here. Most of the answers when you are looking at this is to use a factory which invariably looks like this:

switch(key)
    {
        case 1:
           return serviceProvider.GetService<Service1>();
        case 2:
           return serviceProvider.GetService<Service2>();
        default:
            throw new Exception("Not valid key");
    }

The rest of that answer had a function injected which will work and is what I had done in the past but we want to get away from hard coding.

What is the problem, Really?

The data I am working with is making API calls to various different suppliers to check the status of our orders.

public void DoStuff(IEnumerable<Order> orders)
{
    foreach(var order in orders)
    {
        //look at the order.SuplierId and call their api.
    }
}

This pattern has come up several times and AutoFac made the problem pretty easy to handle with named instances. That case was checking one of 9 different billing systems (2 completely different providers with 9 different versions between them and each version had its quirks) for customer data (lots of mergers).

This time I didn’t want to rip out Microsoft.Extensions.DependencyInjection to make this one thing work (since I would need to replace it in all our projects, yay code reuse) so I needed a factory that was dynamic, could use configuration data to handle which supplier instance I used, and was quick.

Starting to solve the problem

I started this by creating a standard interface for all the supplier api managers to implement.

    public interface ISupplierProcessor
    {
        //Async code is fun.
        Task<ProcessorResponse> DoTheThing(Order data);
    }

After implementing all the ISupplierProcessor (14 of them) there were a couple using the same generic processor but most were very specific to their suppliers.

To get things going I went with a factory class (makes the injection look cleaner to my eyes) to resolve all the necessary instances and a static switch statement. It worked but was wonky and adding new suppliers (if they could use existing implementations) would take code changes.

There was the additional added complexity of registering all the various types and 14 lines of registering things seemed like too much.

Lets Get Dynamic

The first thing I did to handle registering all the classes was to use reflection and register any class that implemented ISupplierProcessor

foreach (var type in AppDomain.CurrentDomain
                              .GetAssemblies()
                              .SelectMany(x => x.GetTypes())
                              .Where(x => typeof(ISupplierProcessor).IsAssignableFrom(x) &&
                                          !x.IsInterface &&
                                          !x.IsAbstract))
{
    services.TryAddScoped(type);
}

This also lets me add ISupplierProcessor in different DLL’s (such as a PROD or DEV project for the implementation dll or some other way to differentiate.) That may be a YAGNI violation but it was quick and easy and gives some benefit.

The next step was to handle the configuration piece. Using Microsoft.Extensions.Configuration and IOptions<T> made getting the config info easy.

I added a config dictionary of the SuplierId and the type name of the implementation

appsettings.json

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "OrderChecker": {
    "ImplementationConfig": {
      "SupplierMapping": {
        "suplier1": "suplier1SupplierProcessor",
        "suplier2": "commonSystemSupplierProcessor",
        "suplier3": "suplier3SupplierProcessor",
        "suplier4": "suplier4SupplierProcessor",
        "suplier5": "suplier5SupplierProcessor",
        "suplier6": "commonSystemSupplierProcessor",
        "suplier7": "commonSystemSupplierProcessor",

...
      }
    }
  }
}

Added an options class

public class SupplierProcessorFactoryOptions
{
    public Dictionary<string,string> SupplierMapping{ get; set; }
}

Then using IServiceCollection.Configure<T>() bound the SupplierProcessorFactoryOptions to the config file'

services.Configure<SupplierProcessorFactoryOptions
>(options => configuration.GetSection("OrderChecker:ImplementationConfig")
.Bind(options));

Taking it on home

Since we now have all our configuration set up and instances registered how can we replace our switch statement. Since we have a dictionary with our class names, we need to get a dictionary with our types. What better way than using similar reflection to when we registered all our instances.

_typeDict = AppDomain.CurrentDomain.GetAssemblies()
                     .SelectMany(x => x.GetTypes())
                     .Where(x => typeof(ICarrierImgDownloader).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract)
                     .ToDictionary(x => x.Name,
                                        x => x);

With all the various pieces of information put together its time to get our types. This isn’t the greatest feeling since its still has a bit of service locator to it, but at least its only in one place and I could inject a different instance of the SupplierProcessorFactory if I wanted to (great for testing).

To put it all together, here is the full SupplierProcessorFactory

public class SupplierProcessorFactory: ISupplierProcessorFactory
{

    private readonly IServiceProvider                  _serviceProvider;
    private readonly ILogger<SupplierProcessorFactory> _logger;
    private readonly SupplierProcessorFactoryOptions   _options;
    private readonly Dictionary<string, Type>          _typeDict;


    public SupplierProcessorFactory(IServiceProvider                          serviceProvider,
                                    ILogger<SupplierProcessorFactory>         logger,
                                    IOptions<SupplierProcessorFactoryOptions> options)
    {
        _serviceProvider = serviceProvider;
        _logger          = logger;
        _options         = options.Value;

        _typeDict = AppDomain.CurrentDomain
                             .GetAssemblies()
                             .SelectMany(x => x.GetTypes())
                             .Where(x => typeof(ISupplierProcessor).IsAssignableFrom(x) &&
                                         !x.IsInterface &&
                                         !x.IsAbstract))
                             .ToDictionary(x => x.Name,
                                                x => x);
    }

    public ISupplierProcessor GetSupplierProcessor(string suplierId)
    {
        if (_options.CarrierIdDownloaderMapping.TryGetValue(suplierId,
                                                            out var processor))
        {
            if (_typeDict.TryGetValue(processor,
                                      out var type))
            {
                return (ISupplierProcessor ) _serviceProvider.GetService(type);
            }

        }
        return null;
    }
}

But did you stop to think if you should

When I first started this I began to wonder if I really needed all this. The answer is yes. It allows my non technical support staff to quickly add new suppliers if they are using systems that we have already interfaced with. There are only a finite number of back end systems and the popular ones are used by quite a few companies. Now that this is all set up, I don’t have to push a code release just to support a new supplier. If we ever get to the point we need to build a UI for supplier onboarding then even completely non technical users can add suppliers with out any IT intervention for the most part.

Always remember, Programmers are the most productive lazy people on the planet.

Recent posts

See more

Categories

About

An ADHD programmer writing about the random stuff they run into. Check the about page for more.