Search code examples
c#dependency-injectionioc-containersimple-injector

How to combine open generic and partially closed generic registrations in Simple Injector


I have the following interface

public interface IMapper<in TSource, out TDestination>
{
    TDestination Map(TSource source);
}

With a default (fallback) implementation:

public class DefaultMapper<TSource, TDestination> : IMapper<TSource, TDestination>
{
   ...
}

And have registered this as follows with Simple Injector:

container.Register(typeof(IMapper<,>), MapperAssemblies);
container.RegisterConditional(typeof(IMapper<,>), typeof(DefaultMapper<,>),
    Lifestyle.Singleton,
    c => !c.Handled);

This allows me to write specific mappers for specific cases and it results in an instance of DefaultMapper being returned from the container whenever an explicit registration (in MapperAssemblies) is missing. Great!

However, a lot of overlap exists in mapping collections or other open generic classes. I would like to avoid writing a separate implementation for every map from, for instance, a collection to collection. How can I setup/adjust my code and register it so Simple Injector will return a:

IMapper<List<TSource>, List<TDestination>> 

or

IMapper<Source<T>, Destination<T>>

when requested. I've tried to apply the partially closed registrations (as per the documentation), this works for certain scenario's, but I lose the generic type which makes mapping a lot harder.


Solution

  • With Simple Injector, this would be a matter of defining a generic mapper implementation that allows mapping from the source list to the destination list, like this:

    public class ListMapper<TSource, TDestination>
        : IMapper<List<TSource>, List<TDestination>>
    {
        private readonly IMapper<TSource, TDestination> mapper;
    
        public ListMapper(IMapper<TSource, TDestination> mapper) => this.mapper = mapper;
    
        public List<TDestination> Map(List<TSource> source) =>
            source.Select(this.mapper.Map).ToList();
    }
    

    You can register this mapper as follows:

    container.Register(typeof(IMapper<,>), MapperAssemblies);
    container.Register(typeof(IMapper<,>), typeof(ListMapper<,>));
    
    // Register last
    container.RegisterConditional(typeof(IMapper<,>), typeof(DefaultMapper<,>),
        Lifestyle.Singleton,
        c => !c.Handled);
    

    Notice how the ListMapper<,> implements IMapper<List<TSource>, List<TDestination>>? This has the identical effect of having a generic type constraint, which allows Simple Injector to apply the mapper conditionally.

    And if you really want to get fancy and want to be able to map any arbitrary collection type to any other arbitrary collection type, you can define the following generic mapper:

    public class EnumerableMapper<TIn, TInCollection, TOut, TOutCollection>
        : IMapper<TInCollection, TOutCollection>
        where TInCollection : IEnumerable<TIn>
        where TOutCollection : IEnumerable<TOut>, new()
    {
        private readonly IMapper<TIn, TOut> mapper;
    
        public EnumerableMapper(IMapper<TIn, TOut> mapper) => this.mapper = mapper;
    
        public TOutCollection Map(TInCollection source) => ...;
    }
    

    Even though this class contains two extra generic types on top of the IMapper abstraction, Simple Injector is still able to figure out what all the types should be. You can register this type as follows:

    container.Register(typeof(IMapper<,>), typeof(EnumerableMapper<,,,>));
    

    The only hard part is to implement its Map method correctly, which can be daunting as the return type can be anything that implements IEnumerable<T>, while you should still be able to create it.