web-dev-qa-db-fra.com

Microsoft Unity. Comment spécifier un certain paramètre dans le constructeur?

J'utilise Microsoft Unity. J'ai une interface ICustomerService et son implémentation CustomerService. Je peux les enregistrer pour le conteneur Unity en utilisant le code suivant:

container.RegisterType<ICustomerService, CustomerService>(new TransientLifetimeManager());

Si CustomerService a un certain paramètre dans son constructeur (par exemple, ISomeService1), j'utilise le code suivant (je dois spécifier SomeService1):

container.RegisterType<ICustomerService, CustomerService>(new TransientLifetimeManager(), new InjectionConstructor(new SomeService1()));

Pas de problèmes ici. 

Le problème apparaît lorsque la classe CustomerService a deux paramètres (pas un paramètre comme dans l'exemple précédent) dans son constructeur (par exemple, ISomeService1 et ISomeService2). Cela fonctionne bien lorsque j'utilise le code suivant: container.RegisterType<ICustomerService, CustomerService>(new TransientLifetimeManager(), new InjectionConstructor(new SomeService1(), new SomeService2()));

Le problème est que je ne veux pas spécifier SomeService2() pour le deuxième paramètre. Je veux spécifier uniquement le premier paramètre - SomeService1(). Mais j'obtiens l'erreur de ne spécifier aucun paramètre ou les deux. 

Comment puis-je spécifier uniquement le premier paramètre du constructeur?

33
Andrei M

Votre meilleure réponse est d’utiliser réellement le conteneur.

Ce que vous faites, c'est "lors de la construction de ce type, utilisez cette instance spécifique de l'objet." Cela ne tire pas parti de la capacité du conteneur à créer une instance pour vous. Au lieu de cela, vous devez enregistrer IService1 et IService2 dans le conteneur. Ensuite, demandez au conteneur de résoudre ces dépendances pour vous.

Cela ressemble à quelque chose comme ça:

container.RegisterType<IService1, SomeService1>();
container.RegisterType<IService2, SomeService2>();

Cela signifie que le conteneur "chaque fois qu'il existe une dépendance de type IService1, crée un nouvel objet de type SomeService1 et le lui remet", et de même pour IService2.

Ensuite, vous devez indiquer au conteneur quoi faire à propos de ICustomerService. En général, vous feriez ceci:

container.RegisterType<ICustomerService, CustomerService>(
    // Note, don't need to explicitly say transient, that's the default
    new InjectionConstructor(new ResolvedParameter<IService1>(),
        new ResolvedParameter<IService2>()));

Cela indique au conteneur: lors de la résolution d'ICustomerService, de créer une instance de CustomerService à l'aide du constructeur prenant IService1 et IService2. Pour obtenir ces paramètres, rappelez dans le conteneur pour résoudre ces types.

C'est un peu bavard, et un cas commun, donc il y a quelques raccourcis. Tout d'abord, vous pouvez passer un objet Type au lieu de créer un nouveau ResolvedParameter, comme suit:

container.RegisterType<ICustomerService, CustomerService>(
    new InjectionConstructor(typeof(IService1), typeof (IService2)));

Pour résumer, si CustomerService n’a qu’un seul constructeur, ou si celui que vous voulez appeler est celui qui prend la liste de paramètres la plus grande, vous pouvez laisser InjectConstructor complètement, car c’est le constructeur choisi par le conteneur en l’absence configuration.

container.RegisterType<ICustomerService, CustomerService>();

Le formulaire que vous utilisez est généralement utilisé lorsque vous souhaitez qu'une valeur spécifique soit transmise pour un paramètre de constructeur plutôt que de résoudre le service par le biais du conteneur.

Pour répondre à votre question initiale, vous ne pouvez pas faire exactement ce que vous avez dit. Le paramètre constructeur a besoin d'une valeur. Vous pouvez toutefois y insérer tout ce que vous voulez - null fonctionne normalement.

Notez que vous pouvez également mélanger les deux formes. Par exemple, si vous voulez résoudre IService1 et transmettre null pour le paramètre IService2, procédez comme suit:

container.RegisterType<ICustomerService, CustomerService>(
    new InjectionConstructor(typeof(IService1), null));

* MODIFIER *

En vous basant sur le commentaire ci-dessous, ce que vous voulez en réalité est une autre fonctionnalité - les inscriptions nommées.

En gros, vous avez deux implémentations d'IService1 et une d'IService2. Vous pouvez donc enregistrer les deux, puis indiquer au conteneur le conteneur à utiliser.

Tout d'abord, pour enregistrer la deuxième implémentation, vous devez donner un nom explicite:

container.RegisterType<IService1, OtherService1Impl>("other");

Ensuite, vous pouvez indiquer au conteneur de résoudre IService1, mais utilisez le nom. C'est la raison principale pour laquelle le type ResolvedParameter existe. Puisque vous voulez juste la valeur par défaut pour IService2, vous pouvez utiliser typeof () comme raccourci. Vous devez toujours spécifier les deux types pour les paramètres, mais vous n'avez pas besoin d'une valeur spécifique. Si cela a du sens.

container.RegisterType<ICustomerService, CustomerService>(
    new InjectionConstructor(new ResolvedParameter<IService1>("other"), typeof(IService2));

Cela devrait faire ce dont vous avez besoin.

64
Chris Tavares

Au lieu de la réponse de Chris Tavares, vous pouvez laisser le conteneur ne résoudre que le second paramètre:

container.RegisterType<ICustomerService, CustomerService>(
    new InjectionConstructor(new SomeService1(), new ResolvedParameter<IService2>());
13
onof

Chris Tavares a donné une bonne réponse avec beaucoup d'informations.

Si vous avez plusieurs paramètres à injecter, il s'agit généralement d'interfaces ou d'instances pouvant être résolues par Unity (à l'aide des techniques differnet). Mais que se passe-t-il si vous souhaitez fournir un seul paramètre qui ne peut pas être résolu automatiquement, par ex. une chaîne pour un nom de fichier?

Maintenant, vous devez fournir tous les typeof(IMyProvider) et une chaîne ou une instance. Mais pour être honnête, fournir uniquement les types peut être fait par Unity, car Unity a déjà une stratégie pour choisir le meilleur ctor.

J'ai donc codé un remplaçant pour InjectionConstructor:

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.Practices.ObjectBuilder2;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.ObjectBuilder;
using Microsoft.Practices.Unity.Utility;

namespace Microsoft.Practices.Unity
{
    /// <summary>
    /// A class that holds the collection of information for a constructor, 
    /// so that the container can be configured to call this constructor.
    /// This Class is similar to InjectionConstructor, but you need not provide
    /// all Parameters, just the ones you want to override or which cannot be resolved automatically
    /// The given params are used in given order if type matches
    /// </summary>
    public class InjectionConstructorRelaxed : InjectionMember
    {
        private List<InjectionParameterValue> _parameterValues;

        /// <summary>
        /// Create a new instance of <see cref="InjectionConstructor"/> that looks
        /// for a constructor with the given set of parameters.
        /// </summary>
        /// <param name="parameterValues">The values for the parameters, that will
        /// be converted to <see cref="InjectionParameterValue"/> objects.</param>
        public InjectionConstructorRelaxed(params object[] parameterValues)
        {
            _parameterValues = InjectionParameterValue.ToParameters(parameterValues).ToList();
        }

        /// <summary>
        /// Add policies to the <paramref name="policies"/> to configure the
        /// container to call this constructor with the appropriate parameter values.
        /// </summary>
        /// <param name="serviceType">Interface registered, ignored in this implementation.</param>
        /// <param name="implementationType">Type to register.</param>
        /// <param name="name">Name used to resolve the type object.</param>
        /// <param name="policies">Policy list to add policies to.</param>
        public override void AddPolicies(Type serviceType, Type implementationType, string name, IPolicyList policies)
        {
            ConstructorInfo ctor = FindExactMatchingConstructor(implementationType);
            if (ctor == null)
            {
                //if exact matching ctor not found, use the longest one and try to adjust the parameters.
                //use given Params if type matches otherwise use the type to advise Unity to resolve later
                ctor = FindLongestConstructor(implementationType);
                if (ctor != null)
                {
                    //adjust parameters
                    var newParams = new List<InjectionParameterValue>();
                    foreach (var parameter in ctor.GetParameters())
                    {
                        var injectionParameterValue =
                            _parameterValues.FirstOrDefault(value => value.MatchesType(parameter.ParameterType));
                        if (injectionParameterValue != null)
                        {
                            newParams.Add(injectionParameterValue);
                            _parameterValues.Remove(injectionParameterValue);
                        }
                        else
                            newParams.Add(InjectionParameterValue.ToParameter(parameter.ParameterType));
                    }
                    _parameterValues = newParams;
                }
                else
                {
                    throw new InvalidOperationException(
                        string.Format(
                            CultureInfo.CurrentCulture,
                            "No constructor found for type {0}.",
                            implementationType.GetTypeInfo().Name));
                }
            }
            policies.Set<IConstructorSelectorPolicy>(
                new SpecifiedConstructorSelectorPolicy(ctor, _parameterValues.ToArray()),
                new NamedTypeBuildKey(implementationType, name));
        }



        private ConstructorInfo FindExactMatchingConstructor(Type typeToCreate)
        {
            var matcher = new ParameterMatcher(_parameterValues);
            var typeToCreateReflector = new ReflectionHelper(typeToCreate);

            foreach (ConstructorInfo ctor in typeToCreateReflector.InstanceConstructors)
            {
                if (matcher.Matches(ctor.GetParameters()))
                {
                    return ctor;
                }
            }

            return null;
        }

       private static ConstructorInfo FindLongestConstructor(Type typeToConstruct)
        {
            ReflectionHelper typeToConstructReflector = new ReflectionHelper(typeToConstruct);

            ConstructorInfo[] constructors = typeToConstructReflector.InstanceConstructors.ToArray();
            Array.Sort(constructors, new ConstructorLengthComparer());

            switch (constructors.Length)
            {
                case 0:
                    return null;

                case 1:
                    return constructors[0];

                default:
                    int paramLength = constructors[0].GetParameters().Length;
                    if (constructors[1].GetParameters().Length == paramLength)
                    {
                        throw new InvalidOperationException(
                            string.Format(
                                CultureInfo.CurrentCulture,
                                "The type {0} has multiple constructors of length {1}. Unable to disambiguate.",
                                typeToConstruct.GetTypeInfo().Name,
                                paramLength));
                    }
                    return constructors[0];
            }
        }
        private class ConstructorLengthComparer : IComparer<ConstructorInfo>
        {
            /// <summary>
            /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
            /// </summary>
            /// <param name="y">The second object to compare.</param>
            /// <param name="x">The first object to compare.</param>
            /// <returns>
            /// Value Condition Less than zero is less than y. Zero equals y. Greater than zero is greater than y.
            /// </returns>
            [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Justification = "Validation done by Guard class")]
            public int Compare(ConstructorInfo x, ConstructorInfo y)
            {
                Guard.ArgumentNotNull(x, "x");
                Guard.ArgumentNotNull(y, "y");

                return y.GetParameters().Length - x.GetParameters().Length;
            }
        }
    }
}

Usage:

container.RegisterType(new TransientLifetimeManager(), new InjectionConstructorRelaxed(
    new SomeService1("with special options")
    //, new SomeService2() //not needed, normal unity resolving used
    //equivalent to: , typeof(SomeService2)
    ));
8
BerndK

Vous pouvez utiliser la hiérarchie des conteneurs. Enregistrez l’implémentation commune dans le conteneur parent, cette instance sera résolue si elle est résolue via le conteneur maître. Créez ensuite un conteneur enfant et enregistrez une implémentation alternative dans un conteneur enfant. Cette mise en œuvre sera résolue Si résolu par le biais du conteneur enfant, l’inscription dans le conteneur enfant annule l’enregistrement dans le conteneur parent.

Voici l'exemple:

public interface IService {}

public interface IOtherService {}

// Standard implementation of IService
public class StandardService : IService {}

// Alternative implementaion of IService
public class SpecialService : IService {}

public class OtherService : IOtherService {}

public class Consumer
{
    public Consumer(IService service, IOtherService otherService)
    {}
}

private void Test()
{
    IUnityContainer parent = new UnityContainer()
        .RegisterType<IService, StandardService>()
        .RegisterType<IOtherService, OtherService>();

    // Here standardWay is initialized with StandardService as IService and OtherService as IOtherService
    Consumer standardWay = parent.Resolve<Consumer>();

    // We construct child container and override IService registration
    IUnityContainer child = parent.CreateChildContainer()
        .RegisterType<IService, SpecialService>();

    // And here specialWay is initialized with SpecialService as IService and still OtherService as IOtherService
    Consumer specialWay = child.Resolve<Consumer>();

    // Profit!
}

Veuillez noter qu'en utilisant la hiérarchie des conteneurs, vous ne pouvez rien connaître du nombre de paramètres dans le constructeur, et c'est très bien, car tant que vous êtes lié au nombre de paramètres et à leurs types, vous ne pouvez pas utiliser toute la puissance d'IoC. Moins vous en savez, mieux c'est.

2
Dmitrii Lobanov

Vous pouvez définir par défaut le second paramètre sur le constructeur (par exemple, =null) ou proposer un constructeur à paramètre unique en plus du constructeur à deux paramètres en le surchargeant.

0
Michael Goldshteyn