简体   繁体   中英

Is it possible to register a generic interface without specifying generic types in Unity?

Let's say I have the following:

 public interface IDataTranslator<TFrom, TTo>  {
        TTo Translate(TFrom fromObj);
    }

  public class IdentityDataTranslator<T> : IDataTranslator<T, T> {
        public T Translate(T fromObj) {
            return fromObj;
        }
    }

I am looking for a way where I can do:

   IdentityDataTranslator<A> trA = UnityContainer.Resolve<IDataTranslator<A, A>>()
   IdentityDataTranslator<B> trB = UnityContainer.Resolve<IDataTranslator<B, B>>()
   IdentityDataTranslator<C> trc = UnityContainer.Resolve<IDataTranslator<C, C>>()

And for all these resolves (assume I don't know the types of the generics that will be resolved) I would like Unity to return an instance of the IdentityDataTranslator with the appropriate generic. Is there a way to register somehow with Unity to achieve this?

You could match the number of generic parameters between the interface and class by creating a new interface which inherits the original interface that had two generic parameters. It is assumed that those two parameters are known to always be the same.

using Microsoft.Practices.Unity;

namespace ConsoleApp
{
    class Program
    {
        static IUnityContainer container;
        static void Main(string[] args)
        {
            container = new UnityContainer();
            container.RegisterType(typeof(IDataTranslator<>), typeof(IdentityDataTranslator<>));

            IdentityDataTranslator<string> translator = container.Resolve<IDataTranslator<string>>() as IdentityDataTranslator<string>;
            string result = translator.Translate("12351");
        }
    }

    public interface IDataTranslator<TFrom, TTo>
    {
        TTo Translate(TFrom fromObj);
    }

    public interface IDataTranslator<T> : IDataTranslator<T,T>
    {
    }

    public class IdentityDataTranslator<T> : IDataTranslator<T>
    {
        public T Translate(T fromObj)
        {
            return fromObj;
        }
    }
}

If I right understood your question you can use InjectionFactory to set how to resolve the current type. It looks like this:

    // It's a test implementation of IDataTranslator<From, To>
    public class DataTranslator<TFrom, TTo> : IDataTranslator<TFrom, TTo>
    {
        TTo IDataTranslator<TFrom, TTo>.Translate(TFrom fromObj)
        {
            throw new NotImplementedException();
        }
    }

    ...
    var strResolve = "notSameResolve";
    container.RegisterType(typeof(IDataTranslator<,>), typeof(DataTranslator<,>), strResolve);
    container.RegisterType(typeof(IDataTranslator<,>),
        new InjectionFactory(
            (con, type, str) =>
            {
                var argumets = type.GetGenericArguments();
                if (argumets[0] != argumets[1])
                {
                    return con.Resolve(type, strResolve);
                }

                return con.Resolve(typeof(IdentityDataTranslator<>).MakeGenericType(type.GetGenericArguments()[0]));
            }));

    var trA = (IdentityDataTranslator<A>)container.Resolve<IDataTranslator<A, A>>();
    var trAData = (DataTranslator<A, B>)container.Resolve<IDataTranslator<A, B>>();

So if you try to resolve IDataTranslator<A, B> with InjectionFactory that is above, you get a DataTranslator<A, B> and get IdentityDataTranslator<A> when try to resolve IDataTranslator<A, A> with the same arguments.

Here's a slightly different approach to your question. Instead of focusing entirely on registration let's also consider how to resolve to the correct type.

The idea is to register the typical case mapping a IDataTranslator<TFrom, TTo> to a DataTranslator<TFrom, TTo> . The next step is to create a Unity container extension to map the special case where TFrom is the same type as TTo while resolving IDataTranslator<TFrom, TTo> .

Given:

public class A { }

public class B { }

public interface IDataTranslator<TFrom, TTo>
{
    TTo Translate(TFrom fromObj);
}

public class DataTranslator<TFrom, TTo> : IDataTranslator<TFrom, TTo>
{
    public TTo Translate(TFrom fromObj)
    {
        return Activator.CreateInstance<TTo>();
    }
}

public class IdentityDataTranslator<T> : IDataTranslator<T, T>
{
    public T Translate(T fromObj)
    {
        return fromObj;
    }
}

Next create a container extension to handle the IdentityDataTranslator:

public class IdentityGenericsExtension : UnityContainerExtension
{
    private readonly Type identityGenericType;
    private readonly Type baseType;

    public IdentityGenericsExtension(Type identityGenericType, Type baseType)
    {
        // Verify that Types are open generics with the correct number of arguments
        // and that they are compatible (IsAssignableFrom).
        this.identityGenericType = identityGenericType;
        this.baseType = baseType;
    }

    protected override void Initialize()
    {
        this.Context.Strategies.Add(
            new IdentityGenericsBuildUpStrategy(this.identityGenericType, this.baseType),
                UnityBuildStage.TypeMapping);
    }

    private class IdentityGenericsBuildUpStrategy : BuilderStrategy
    {
        private readonly Type identityGenericType;
        private readonly Type baseType;

        public IdentityGenericsBuildUpStrategy(Type identityGenericType, Type baseType)
        {
            this.identityGenericType = identityGenericType;
            this.baseType = baseType;
        }

        public override void PreBuildUp(IBuilderContext context)
        {
            if (context.OriginalBuildKey.Type.IsGenericType &&
                context.OriginalBuildKey.Type.GetGenericTypeDefinition() == this.baseType)
            {
                // Get generic args
                Type[] argTypes = context.BuildKey.Type.GetGenericArguments();

                if (argTypes.Length == 2 && argTypes.Distinct().Count() == 1)
                {
                    context.BuildKey = new NamedTypeBuildKey(
                        this.identityGenericType.MakeGenericType(argTypes[0]),
                        context.BuildKey.Name);
                }
            }
        }
    }
}

What this does is before type mapping check to see if the requested type is a IDataTranslator<T,K> and that there are two generic arguments and both generic arguments are the same type. If so then the IdentityDataTranslator<T> is set as the new build key (instead of the expected DataTranslator<T,K> .

Missing is validation of the types to ensure they are the correct shape and are assignable.

Finally, setup the container and run some tests to ensure we get an IdentityDataTranslator when the from and to types are the same:

var container = new UnityContainer();
container.AddExtension(
    new IdentityGenericsExtension(typeof(IdentityDataTranslator<>), typeof(IDataTranslator<,>)));

container.RegisterType(typeof(IDataTranslator<,>), typeof(DataTranslator<,>));

// Since A is different than B we get back a DataTranslator<A,B>
var dataTranslator = container.Resolve<IDataTranslator<A, B>>();
Debug.Assert(dataTranslator.GetType() == typeof(DataTranslator<A, B>));

// Since A is the same as A we get back a IdentityDataTranslator<A>
var identityTranslator = container.Resolve<IDataTranslator<A, A>>();
Debug.Assert(identityTranslator.GetType() == typeof(IdentityDataTranslator<A>));

The above approach works but there might be a strictly registration based approach that I didn't think of that also works and enforces your constraints.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM