2

In an application using Autofac as its IoC container, I have a generic interface with two type parameters:

public interface IMapper<TSource, TTarget>
{
    TTarget GetTarget(TSource source);
}

and a wrapper interface to dynamically select the appropriate IMapper<TSource, TTarget> depending on its input parameter types:

public interface IDynamicMapper
{
    T GetTarget<T>(object source);
}

I want my implementation of IDynamicMapper to find at runtime the appropriate IMapper<TSource, TTarget> component, using Autofac, which has a TSource equal to source.GetType() and TTarget being a derived type of T (or T itself):

public class DynamicMapper : IDynamicMapper
{
    private readonly ILifetimeScope _scope;

    public DynamicMapper(ILifetimeScope scope)
    {
        this._scope = scope;
    }

    T IDynamicMapper.GetTarget<T>(object source)
    {
        Type sourceType = source.GetType();
        Type targetBaseType = typeof(T);

        //TODO: find an IMapper<TSource, TTarget> implementation where
        // 1) Condition on TSource: typeof(TSource) == sourceType
        // 2) Condition on TTarget: targetBaseType.IsAssignableFrom(typeof(TTarget))
        // Many implementations can potentially match these criterias,
        // choose the 1st one
        // (this should not happen if the application is designed correctly)

        if (mapper == null)
        {
            throw new ArgumentException(
                "Could not find an IMapper<TSource, TTarget> implementation" +
                " for the supplied parameters"
            );
        }

        // call mapper.GetTarget(source) and return the result
        // (mapper is probably dynamic, but its runtime type implements
        // TTarget IMapper<TSource, TTarget>.GetTarget(TSource source))
    }
}

All my components are registered to the Autofac container as their service interfaces in another part of the application (using assembly scanning for the record).


UPDATE 1

Based on Steven's pertinent answers I updated my interface as follow to use variance:

public interface IMapper<in TSource, out TTarget>
{
    TTarget GetTarget(TSource source);
}

My dynamic mapper's GetTarget() method looks like this:

T IDynamicMapper.GetTarget<T>(object source)
{
    Type sourceType = source.GetType();
    Type targetBaseType = typeof(TTarget);
    Type mapperType = typeof(IMapper<,>).MakeGenericType(sourceType, targetBaseType);

    // This fails with ComponentNotRegisteredException
    dynamic mapper = this._scope.Resolve(mapperType);

    // This also fails (mapper is null):
    // IEnumerable<object> mappers = (IEnumerable<object>)this._scope.Resolve(typeof(IEnumerable<>).MakeGenericType(mapperType));
    // dynamic mapper = mappers.FirstOrDefault();

    // Invoke method
    return mapper.GetTarget((dynamic)source);
}

However, when calling Resolve(mapperType) or Resolve(typeof(IEnumerable<>).MakeGenericType(mapperType)), the component is not resolved, although it is present in the container's registrations, mapped to the service IMapper<TSource, TTarget>. The 1st call throws an exception and the 2nd one returns an empty enumerable.

Maxime Rossini
  • 3,612
  • 4
  • 31
  • 47
  • Related: https://stackoverflow.com/questions/7323789/autofac-resolving-variant-types-with-both-in-and-out-type-arguments – Steven Sep 11 '15 at 07:49

2 Answers2

4

This should do the trick:

T IDynamicMapper.GetTarget<T>(object source) {

    Type mapperType = typeof(IMapper<,>).MakeGenericType(source.GetType(), typeof(T));

    // Will throw when no registration exists.
    // Note the use of 'dynamic'.
    dynamic mapper = scope.Resolve(mapperType);

    return (T)mapper.GetTarget<T>((dynamic)source);
}
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Just tried this: 3 problems occured. 1st I needed to switch to implicit interface implementation in my `IMapper<,>` implementation class (dynamic does not work well with explicit interface implementation it seems), then I needed to replace the last call with `return mapper.GetTarget((dynamic)source);`, and last but not least, this solution does not work if `TTarget` is a subclass of `T` and not `T` itself! The `Resolve()` call throws an exception in this case. – Maxime Rossini Sep 10 '15 at 09:35
  • 1
    @MaximeRossini: About 1. Yes, this is a bizarre limitation of dynamic that I happened to stumble upon in the past. What I did was wrap the mapper type in a decorator-like type that is both implicitly implemented and a public type (dynamic will fail on an internal type, even though the interface is public). You could even let Autofac resolve the `MapperWrapper<,>` for you. – Steven Sep 10 '15 at 09:43
  • @MaximeRossini: About 3. This will not work, since `IMapper<,>` is not variant. You will have to define it as `IMapper`. And in that case you should resolve a collection of mappers, and have to decide what to do when multiple instances are resolved; which one to select? – Steven Sep 10 '15 at 09:45
  • About 3. This is exactly what I want to do (have a collection of resolved components and select the first one). I just tried to make the `TTarget` type parameter covariant (`out` modifier) but it does not prevent the `ComponentNotRegisteredException` when calling `Resolve()`... – Maxime Rossini Sep 10 '15 at 09:56
  • About 1. This is actually a normal behaviour, as dynamic objects have only access to their actual type and base type members, not to the interface members (like any other object, you have to cast it to the interface type to have acess to the interface members - how would a dynamic choose between an implicit implementation and an explicit implementation otherwise?). But it is possible to make this work with explicit implementations (and without use of dynamic!) by using reflection to get the interface `MethodInfo` from the component. – Maxime Rossini Sep 10 '15 at 10:02
  • @MaximeRossini: you should call `Resolve(typeof(IEnumerable<>).MakeGenericType(mapperType))` to get a collection. But what exactly is "the first one"? Autofac doesn't guarantee the order in which elements of the collection. – Steven Sep 10 '15 at 10:03
  • @MaximeRossini: But it is impossible to cast to an interface if it is generic, while the generic arguments are unknown at compile time (as is in your case). Please be warned about `MethodInfo.Invoke`. `Invoke` will wrap any thrown exception in an `InvocationException` which can complicate debugging or handling of exceptions higher up the call stack. `dynamic` doesn't wrap thrown exception which can be really beneficial. This might not be such an issue with a mapper, but important to realize. – Steven Sep 10 '15 at 10:07
  • Calling `Resolve(typeof(IEnumerable<>).MakeGenericType(mapperType))` returns an empty enumerable. See my updated answer for details on the way I register my components and the modifications I made to the interface. – Maxime Rossini Sep 10 '15 at 10:55
  • @MaximeRossini: What I remember is that Autofac will only resolve variant interfaces for you when the interface has exactly one `in` parameter (and no `out` parameters). I believe you can override this behavior, but I have no idea how. You'll need to consult an Autofac expert on this (which I am not). – Steven Sep 10 '15 at 11:37
  • Yes, I saw that Steven is the creator of Simple Injector so I tried it when I read it supported variance out of the box. It works much better with covariance and contravariance than Autofac and elegantly solves this specific case, but it is unfortunately too restrictive in its design to allow us switching to it right now (our application does not exactly respect all of the SOLID principles as you might suspect) :/ Some nice shortcut features of Autofac would be missing too (like startable components, assembly scanning, etc.). – Maxime Rossini Sep 10 '15 at 15:06
  • 1
    Assembly scanning in Simple Injector can be done by using `Container.GetTypesToRegister` or by following [this guide](https://simpleinjector.readthedocs.org/en/latest/advanced.html#batch-registration). I'm not completely sure what the function of startable components is, but I think that you can implement it using [Packages](https://www.nuget.org/packages/SimpleInjector.Packaging/), although it would probably be trivial to hand-code this. – Steven Sep 10 '15 at 15:20
3

Autofac does not support covariant generic types (ISomething<out T>). Another IoC container like Simple Injector could do the trick in this case, but to make it work with Autofac I ended up using another interface:

Services:

public interface IMapper<TSource, out TTarget> : IMapperLocator<TSource>
{
    TTarget Extract(TSource source);
}
public interface IMapperLocator<TSource>
{
}
public interface IDynamicMapper
{
    T Extract<T>(object source);
}

Implementation:

public class DynamicMapper : IDynamicMapper
{
    private readonly ILifetimeScope _scope;

    public DynamicMapper(ILifetimeScope scope)
    {
        this._scope = scope;
    }

    T IDynamicMapper.Extract<T>(object source)
    {
        // Get useful types
        Type sourceType = source.GetType();
        Type targetBaseType = typeof(TTarget);
        Type mapperBaseType = typeof(IMapper<,>).MakeGenericType(sourceType, targetBaseType);
        Type locatorType = typeof(IMapperLocator<>).MakeGenericType(sourceType);
        Type enumType = typeof(IEnumerable<>).MakeGenericType(locatorType);

        // Get all mapper implementations that take a TSource with the
        // same type as the source object
        var mappers = (IEnumerable<object>)this._scope.Resolve(enumType);

        // Among all the mappers with the right TSource, select the one
        // with TTarget assignable to T (throws if there is 0 or more than
        // one mapper, as this would be an implementation error)
        dynamic mapper = mappers.Single(x => mapperBaseType.IsAssignableFrom(x.GetType()));

        // The method must implemented implicitly.
        // A workaround would be to use a wrapper (IMapperWrapper<TSource, out TTarget>)
        // that implements the method implicitly and invokes the mapper's method
        // without using dynamic
        return mapper.Extract((dynamic)source);
    }
}
Maxime Rossini
  • 3,612
  • 4
  • 31
  • 47