web-dev-qa-db-fra.com

Injection de dépendance dans les classes de modèle (entités)

Je construis une application ASP.NET Core MVC avec Entity Framework Code-First. J'ai choisi d'implémenter un modèle de référentiel simple, fournissant des opérations CRUD de base pour toutes les classes de modèle que j'ai créées. I a choisi de suivre toutes les recommandations fournies dans http://docs.asp.net et DI en fait partie.

Dans .NET 5, l'injection de dépendance fonctionne très bien pour toute classe que nous n'instancions pas directement (par exemple: contrôleurs, référentiels de données, ...).

Nous les injectons simplement via le constructeur et enregistrons les mappages dans la classe de démarrage de l'application:

// Some repository class
public class MyRepository : IMyRepository
{
    private readonly IMyDependency _myDependency;
    public MyRepository(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }
}

// In startup.cs :
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyRepository, MyRepository>();

Le problème que j’ai, c’est que, dans certaines de mes classes de modèles, j’aimerais insérer certaines des dépendances que j’ai déclarées.

Mais je pense que je ne peux pas utiliser le modèle d'injection du constructeur, car les classes de modèle sont souvent explicitement instanciées. Par conséquent, je devrais me fournir moi-même les dépendances, ce que je ne peux pas.

Ma question est donc la suivante: existe-t-il un autre moyen que l’injection de constructeur d’injecter des dépendances, et comment? Je pensais par exemple à un modèle d'attribut ou quelque chose comme ça.

12
kall2sollies

Comme je l'ai déjà expliqué dans un commentaire, lors de la création d'un objet avec new, rien dans l'infrastructure d'injection de dépendance n'est impliqué dans le processus. En tant que tel, il est impossible pour le framework DI d’injecter magiquement des choses dans cet objet, il ne le sait tout simplement pas.

Comme cela n’a aucun sens de laisser le cadre DI create vos instances de modèle (les modèles ne sont pas une dépendance ), vous devrez indiquer explicitement vos dépendances si vous voulez le modèle. les avoir. Cela dépend un peu de la façon dont vos modèles sont utilisés et de la nature de ces dépendances.

Le cas simple et clair serait que votre modèle attende simplement les dépendances sur le constructeur. De cette façon, si vous ne les fournissez pas, le temps de compilation est une erreur et le modèle y a immédiatement accès. En tant que tel, quel que soit ce qui précède, la création des modèles, est nécessaire pour avoir les dépendances dont le type de modèle a besoin. Mais à ce niveau, il est probable qu’il s’agit d’un service ou d’un contrôleur ayant accès à DI et pouvant demander la dépendance elle-même.

Bien sûr, selon le nombre de dépendances, cela peut devenir un peu compliqué car vous devez toutes les transmettre au constructeur. Donc, une alternative serait d'avoir une "fabrique de modèles" qui se charge de créer l'objet modèle. Une autre solution consisterait également à utiliser le modèle service locator , en transmettant la variable IServiceCollection au modèle, qui peut alors demander toutes les dépendances dont il a besoin. Notez que c'est généralement une mauvaise pratique et non plus une inversion de contrôle.

Ces deux idées ont pour problème de modifier la manière dont l'objet est créé. Et certains modèles, notamment ceux gérés par Entity Framework, nécessitent un constructeur vide pour que EF puisse créer l'objet. Donc, à ce stade, vous vous retrouverez probablement avec quelques cas où les dépendances de votre modèle sont not résolues (et vous n’avez pas de moyen facile de le dire).

Une méthode généralement meilleure, qui est également beaucoup plus explicite, consisterait à transmettre la dépendance là où vous en avez besoin, par exemple. si vous avez une méthode sur le modèle qui calcule certaines choses mais nécessite une configuration, laissez-la exiger cette configuration. Cela rend également les méthodes plus faciles à tester.

Une autre solution serait de déplacer la logique hors du modèle. Par exemple, les modèles Identité ASP.NET sont vraiment stupides. Ils ne font rien. Toute la logique est effectuée dans le UserStore qui est un service et peut donc avoir des dépendances de service.

6
poke

Le modèle souvent utilisé dans la conception pilotée par domaine (le modèle de domaine riche doit être spécifique) consiste à transmettre les services requis à la méthode que vous appelez.

Par exemple, si vous souhaitez calculer la TVA, vous transmettez le service de TVA à la méthode CalculateVat

Dans votre modèle 

    public void CalculateVat(IVatCalculator vatCalc) 
    {
        if(vatCalc == null)
            throw new ArgumentNullException(nameof(vatCalc));

        decimal vatAmount = vatcalc.Calculate(this.TotalNetPrice, this.Country);
        this.VatAmount = new Currency(vatAmount, this.CurrencySymbol);
    }

Votre classe de service

    // where vatCalculator is an implementation IVatCalculator 
    order.CalculateVat(vatCalculator);

Enfin, votre service peut injecter d’autres services, comme un référentiel qui récupère le taux de taxe d’un pays donné.

public class VatCalculator : IVatCalculator
{
    private readonly IVatRepository vatRepository;

    public VatCalculator(IVatRepository vatRepository)
    {
        if(vatRepository == null)
            throw new ArgumentNullException(nameof(vatRepository));

        this.vatRepository = vatRepository;
    }

    public decimal Calculate(decimal value, Country country) 
    {
        decimal vatRate = vatRepository.GetVatRateForCountry(country);

        return vatAmount = value * vatRate;
    }
}
4
Tseng

Existe-t-il un autre moyen que l'injection de constructeur d'injecter des dépendances, et comment?

La réponse est "non", cela ne peut pas être fait avec "l'injection de dépendance". Mais, "oui", vous pouvez utiliser le "modèle de localisation de service" pour atteindre votre objectif final.

Vous pouvez utiliser le code ci-dessous pour résoudre une dépendance sans utiliser l'injection de constructeur ou l'attribut FromServices. De plus, vous pouvez new créer une instance de la classe comme bon vous semble et cela fonctionnera toujours - en supposant que vous ayez ajouté la dépendance dans le Startup.cs.

public class MyRepository : IMyRepository
{
    public IMyDependency { get; } =
        CallContextServiceLocator.Locator
                                 .ServiceProvider
                                 .GetRequiredService<IMyDependency>();
}

Le CallContextServiceLocator.Locator.ServiceProvider est le fournisseur de services mondial, où tout vit. Il n'est pas vraiment conseillé d'utiliser ceci. Mais si vous n'avez pas d'autre choix, vous le pouvez. Il serait recommandé d'utiliser plutôtDIet de ne jamais instancier manuellement un objet, c'est-à-dire .; éviter new.

2
David Pine

Les classeurs de modèles intégrés se plaignent de ne trouver aucun ctor par défaut. Par conséquent, vous en avez besoin d'un personnalisé.

Vous pouvez trouver une solution à un problème similaire ici , qui inspecte les services enregistrés afin de créer le modèle.

Il est important de noter que les extraits ci-dessous fournissent des fonctionnalités légèrement différentes qui, espérons-le, répondent à vos besoins particuliers. Le code ci-dessous attend des modèles avec des injections de ctor. Bien entendu, ces modèles ont les propriétés habituelles que vous pourriez avoir définies. Ces propriétés sont renseignées exactement comme prévu, de sorte que le bonus est le comportement correct lors de la liaison de modèles avec des injections ctor .

    public class DiModelBinder : ComplexTypeModelBinder
    {
        public DiModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders)
        {
        }

        /// <summary>
        /// Creates the model with one (or more) injected service(s).
        /// </summary>
        /// <param name="bindingContext"></param>
        /// <returns></returns>
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            var services = bindingContext.HttpContext.RequestServices;
            var modelType = bindingContext.ModelType;
            var ctors = modelType.GetConstructors();
            foreach (var ctor in ctors)
            {
                var paramTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList();
                var parameters = paramTypes.Select(p => services.GetService(p)).ToArray();
                if (parameters.All(p => p != null))
                {
                    var model = ctor.Invoke(parameters);
                    return model;
                }
            }

            return null;
        }
    }

Ce cartable sera fourni par:

public class DiModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = context.Metadata.Properties.ToDictionary(property => property, context.CreateBinder);
            return new DiModelBinder(propertyBinders);
        }

        return null;
    }
}

Voici comment le classeur serait enregistré:

services.AddMvc().AddMvcOptions(options =>
{
    // replace ComplexTypeModelBinderProvider with its descendent - IoCModelBinderProvider
    var provider = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
    var binderIndex = options.ModelBinderProviders.IndexOf(provider);
    options.ModelBinderProviders.Remove(provider);
    options.ModelBinderProviders.Insert(binderIndex, new DiModelBinderProvider());
});

Je ne suis pas tout à fait sûr que le nouveau classeur doive être enregistré exactement au même index, vous pouvez expérimenter avec cela.

Et, à la fin, voici comment vous pouvez l’utiliser:

public class MyModel 
{
    private readonly IMyRepository repo;

    public MyModel(IMyRepository repo) 
    {
        this.repo = repo;
    }

    ... do whatever you want with your repo

    public string AProperty { get; set; }

    ... other properties here
}

La classe de modèle est créée par le classeur qui fournit le service (déjà enregistré) et le reste des classeurs de modèle fournit les valeurs de propriété à partir de leurs sources habituelles.

HTH

1
Alexander Christov

Vous pouvez le faire, consultez [InjectionMethod] et container.BuildUp (instance);

Exemple:

Constructeur DI typique (PAS NÉCESSAIRE SI VOUS UTILISEZ InjectionMethod) public ClassConstructor (DeviceHead pDeviceHead) { This.DeviceHead = pDeviceHead; }

Cet attribut provoque l'appel de cette méthode pour configurer DI. [InjectionMethod] public void Initialize (DeviceHead pDeviceHead) { This.DeviceHead = pDeviceHead; }

0
DougS