Search code examples
c#simple-injector

Register same object multiple times with different configuration


I have this api client ICommunicationClient(url, tenant) registered in my IoC container. Now I'm facing the scenario where I can have 1 to n api clients. I need to register all of them and I'm not sure how to handle that. I've seen there's this RegisterCollection in SI though.

I'm considering use a ICommunicationClientProvider as a wrapper around the actual clients. It contains a list with all the registered clients and methods to retrieve them. I feel this is not the best approach and of course, it "forces" me to touch other pieces of the app.

public class CommunicationClientProvider : ICommunicationClientProvider
{
    public CommunicationClientCollection CommunicationClientsCollection { get; set; }
    public string Tenant { get; set; }

    public ICommunicationClient GetClients()
    {
        return CommunicationClientsCollection[Tenant];
    }
    public void SetClients(CommunicationClientCollection clients)
    {
        CommunicationClientsCollection = clients;
    }
}

public interface ICommunicationClientProvider
{
    ICommunicationClient GetClients();
    void SetClients(CommunicationClientCollection clients);
}

This to host the collection

public class CommunicationClientCollection : Dictionary<string, ICommunicationClient>
{
}

Here I register the collection against SI

        var clients = new CommunicationClientProvider();
        foreach (var supportedTenant in supportedTenants)
        {
            clients.CommunicationClientsCollection
                .Add(supportedTenant, new CommunicationClient(
                    new Uri(configuration.AppSettings["communication_api." + supportedTenant]),
                    new TenantClientConfiguration(supportedTenant)));
        }
        container.RegisterSingleton<ICommunicationClientProvider>(clients);

Do you know a better way of doing this? This is a normal scenario for example when you have multiple databases.

UPDATE: - ITenantContext part - This is basically how my tenant context interface looks like:

public interface ITenantContext
{
    string Tenant { get; set; }
}

and this is where I'm making my call to communication api:

public class MoveRequestedHandler : IHandlerAsync<MoveRequested>
{
    private readonly IJctConfigurationService _communicationClient;
    private readonly ITenantContext _tenantContext;

    public MoveRequestedHandler(IJctConfigurationService communicationClient, ITenantContext tenantContext)
    {
        _communicationClient = communicationClient;
        _tenantContext = tenantContext;
    }

    public async Task<bool> Handle(MoveRequested message)
    {
        _tenantContext.Tenant = message.Tenant;
        _communicationClient.ChangeApn(message.Imei, true);

        return await Task.FromResult(true);
    }
}

here I register the ITenantContext

container.RegisterSingleton<ITenantContext, TenantContext>();

The tenant is defined within the MoveRequested object (message.Tenant). How can I make CommunicationClient aware of that tenant?


Solution

  • If adding an ICommunicationClientProvider abstraction causes you to make sweeping changes throughout your application, there is clearly something wrong. You should typically be able to add features and make changes without having to do sweeping changes. And as a matter of fact, I think your current design already allows this.

    Your ICommunicationClientProvider) acts like a factory, and factories are hardly ever the right solution. Instead, your are much better of using the Composite design pattern. For instance:

    sealed class TenantCommunicationClientComposite : ICommunicationClient
    {
        private readonly ITenantContext tenantContext;
        private readonly Dictionary<string, ICommunicationClient> clients;
    
        public TenantCommunicationClientComposite(ITenantContext tenantContext,
            Dictionary<string, ICommunicationClient> clients) {
            this.tenantContext = tenantContext;
            this.clients = clients;
        }
    
        object ICommunicationClient.ClientMethod(object parameter) =>
            this.clients[this.tenantContext.CurrentTenantName].ClientMethod(parameter);
    }
    

    You can register this class as follows:

    var dictionary = new Dictionary<string, ICommunicationClient>();
    foreach (var supportedTenant in supportedTenants) {
        dictionary.Add(supportedTenant, new CommunicationClient(
            new Uri(configuration.AppSettings["communication_api." + supportedTenant]),
            new TenantClientConfiguration(supportedTenant)));
    }
    
    container.RegisterSingleton<ICommunicationClient>(
        new TenantCommunicationClientComposite(
            new AspNetTenantContext(),
            dictionary));
    

    Here the ITenantContext is an abstraction that allows you to get the current tenant on who's behalf the current request is running. The AspNetTenantContext is an implementation that allows you to retrieve the current tenant in an ASP.NET application. You probably already have some code to detect the current tenant; you might need to move that code to such AspNetTenantContext class.