Register dynamic Mapping (or delegate without explicitly resolving)

Viewed 55

I have the following registrations prior to the one my question is about:

container.Register<ISettings>();
container.Register<Connection1>();
container.Register<Connection2>();

Connection1 and Connection2 both implement an the interface IConnection. To simplify my case ISettings just contains a property UseConnection1.

What I want is to register a mapping or delegate for service type IConnection that expects ISettings as a dependency and returns either Connection1 or Connection2 as its implementation type.

RegisterMapping itself does not have such capabilities due to the fact, that one can't use any depending services to specify the mapping dynamically.

RegisterDelegate on the other hand has the capabilities to get a specific depending service, but expects the user to return the implementation itself, not its type, like that:

container.RegisterDelegate<ISettings, IResolver, IConnection>((settings, resolver) =>
{
    var serviceType = settings.UseConnection1 ? typeof(Connection1) : typeof(Connection2);
    return resolver.Resolve<IConnection>(serviceType);
});

As seen, that implementation would be got from the IResolver which is far from perfect, as described in the DryIoc wiki.

What I wish to do is something like this:

container.RegisterSomething<IConnection>((ISettings settings) => settings.UseConnection1 ? typeof(Connection1) : typeof(Connection2));

Is this possible in any way with DryIoc?

2 Answers

The use of runtime data (i.e. Settings.UseConnection1) during object construction leads to the runtime-data DI code smell, which is something you should strive to prevent. Also, the injection of both dependencies should not pose any performance problems as the creation of objects should be fast and reliable, because injection constructors should be simple.

Instead, try using a proxy, e.g.

public class ProxyConnection : IConnection
{
    private readonly ISettings Settings;
    private readonly Connection1 Con1;
    private readonly Connection2 Con2;

    public ProxyConnection(ISettings settings, Connection1 con1, Connection2 con2)
    {
        Settings = settings;
        Con1 = con1;
        Con2 = con2;
    }

    private IConnection Connection => Settings.UseConnection1 ? Con1 : Con2;

    // Implement IConnection methods here, e.g.
    void IConnection.Open() => Connection.Open();
}

// Or use C# 9 to do the same but with less code:
public record ProxyConnection(ISettings Settings, Connection1 Con1, Connection2 Con2)
    : IConnection
{
    private IConnection Connection => Settings.UseConnection1 ? Con1 : Con2;

    // Implement IConnection methods here, e.g.
    void IConnection.Open() => Connection.Open();
}

Now you can register your types as follows:

container.Register<ISettings>();
container.Register<Connection1>();
container.Register<Connection2>();
container.Register<IConnection, ProxyConnection>();

If, however, the Settings is loaded just once at application startup and doesn't change, there is no need to be able to switch between implementations at runtime, and you can reduce your registrations to the following:

if (settings.UseConnection1)
{
   container.Register<IConnection, Connection1>(); 
}
else
{
   container.Register<IConnection, Connection2>(); 
}

In that case there is no need for having the proxy class.

First, I agree with @steven that the runtime data should be treated with care and better be solved outside of DI. Especially, if it is mutable and being used as a condition.

Second, if unavoidable, I would've used the factory delegate from the comments as the least surprise solution: RegisterDelegate<ISettting, IConnection>(settings => /*connection construction*/).

Last, the heads-on answer would be the condition-based resolution and can be done with the condition setup:

The live code

using System;
using DryIoc;
                    
public class Program
{
    public static void Main()
    {
        var container = new Container(); 
        container.Register<ISettings, Settings>(Reuse.Singleton);
        container.Register<IConnection, Connection1>(setup: Setup.With(condition: r => r.Container.Resolve<ISettings>().UseConnection1));
        container.Register<IConnection, Connection2>(setup: Setup.With(condition: r => r.Container.Resolve<ISettings>().UseConnection1 == false));
        
        var settings = container.Resolve<ISettings>();
        settings.UseConnection1 = true;
        var conn1 = container.Resolve<IConnection>();
        Console.WriteLine(conn1.GetType());
        
        settings.UseConnection1 = false;
        var conn2 = container.Resolve<IConnection>();
        Console.WriteLine(conn2.GetType());
    }
    
    public interface ISettings
    {
        bool UseConnection1 { get; set; }
    }
    
    public class Settings : ISettings { public bool UseConnection1 { get; set; } }

    public interface IConnection {}
    public class Connection1 : IConnection {}
    public class Connection2 : IConnection {}
}

Update:

There is another option closer to what you have originally imagined:

container.Register<IConnection>(made: Made.Of(r =>
    r.Container.Resolve<ISettings>().UseConnection1 ? typeof(Connection1) : typeof(Connection2)));
Related