Search code examples
c#dependency-injectionsimple-injector

Dynamically select a concrete object with a generic name using Simple Injector


This example shows the registration and resolving with a simple object hierarchy with SimpleInjector. How this can be solved if I add a generic type (Shipper with a contract) in the hierarchy?

Considerations:

  1. The contract is parsed as a csv file and they can be totally different for UPS and FedEx. Only the UPS and FedEx classes know what to look for in the contracts before shipping. The contracts are read from the csv due to the Business can update the contract anytime and drop it in the folder. The app should not start as a result of changes in the contract.

  2. I want to test UPS and FedEx with its dependencies like notifications etc... (not in the code).

  3. Please don't consider the register and resolve that I have used in the example is wrong. Should consider only the class hierarchy.

The following is the partial code:

public Setup()
{
    var container = new SimpleInjector.Container();
    // TODO: Registration required for two different shippers
    container.Register<UPS, IShipper>();
}

public interface IShipper<T>
{
    T contact;
    void Ship(Product product);
    T ParseContract(string fileName);
    void SetContract(T);
}

public class FiveYearContract
{
    public string IsSigned { get; set; }
}

public class TenYearContract
{
    public string IsPrepaid { get; set; }
}

public interface IUPS<T> : IShipper<T> { }
public interface IFedEx<T> : IShipper<T> { }

public class UPS : IUPS<FiveYearContract> { ... }
public class FedEx : IFedEx<TenYearContract> { ... }

Usage:

public void Ship(String shipper, Product product)
{
    // TODO: Retrieve the shipper by name
    var shipper = container.GetInstance<IShipper>();
    shipper.SetContract(shipper.ParseContract("contract.csv");
    // Contract will be checked before sending
    shipper.Ship(product);
}

Ship("FedEx", new Product());

Solution

  • To be able to resolve a IShipper<T> where the T is unknown at compile time, you will have to use reflection. For instance:

    public Setup()
    {
        container.Register(typeof(IShipper<>), new[] { typeof(FedEx).Assembly });
    }
    
    public void Ship(String shipper, Product product)
    {
        var contract = shipper.ParseContract("contract.csv");
    
        dynamic shipper = container.GetInstance(
            typeof(IShipper<>).MakeGenericType(contract.GetType()));
    
        shipper.SetContract((dynamic)contract);
    
        shipper.Ship(product);
    }
    

    The SetContract method however, causes the shipper implementations to become mutable and it causes Temporal Coupling. A better design would typically be to pass the contract runtime value on with the Ship method, as follows:

    public void Ship(String shipper, Product product)
    {
        var contract = shipper.ParseContract("contract.csv");
    
        dynamic shipper = container.GetInstance(
            typeof(IShipper<>).MakeGenericType(contract.GetType()));
    
        shipper.Ship((dynamic)contract, product);
    }
    

    This allows the IShipper<T> implementations to stay immutable (which can considerably simplify your application) and removes the implicit relationship between the SetContract and Ship methods. It most likely makes those implementations easier to test as well.