Dependency Injection in Model classes (entities)

2020-02-27 10:51发布

I am building an ASP.NET Core MVC application with Entity Framework Code-First. I chose to implement a simple repository pattern, providing basic CRUD operations for all the model classes I have created. I chose to follow all the recommendations provided in http://docs.asp.net and DI is one of these.

In .NET 5, dependency injection works very well for any class that we do not directly instanciate (e.g.: controllers, data repositories, ...).

We simply inject them via the constructor, and register the mappings in the Startup class of the 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>();

The problem I have is that in some of my model classes, I would like to inject some of the dependencies I have declared.

But I think that I cannot use the constructor injection pattern, because model classes are often explicitely instanciated, and therefore, I would need to provide myself the dependencies, which I can't.

So my question is: is there another way than constructor injection to inject dependencies, and how? I was for example thinking of an attribute pattern or something like that.

6条回答
甜甜的少女心
2楼-- · 2020-02-27 11:39

The pattern often used in domain driven design (rich domain model to be specific) is to pass the required services into the method you are calling.

For example if you want to calculate the vat, you'd pass the vat service into the CalculateVat method.

In your model

    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);
    }

Your service class

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

Finally your service can inject another services, like a repository which will fetch the tax rate for a certain country

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;
    }
}
查看更多
神经病院院长
3楼-- · 2020-02-27 11:41

The built-in model binders complain that they cannot find a default ctor. Therefore you need a custom one.

You may find a solution to a similar problem here, which inspects the registered services in order to create the model.

It is important to note that the snippets below provide slightly different functionality which, hopefully, satisfies your particular needs. The code below expects models with ctor injections. Of course, these models have the usual properties you might have defined. These properties are filled in exactly as expected, so the bonus is the correct behavior when binding models with ctor injections.

    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;
        }
    }

This binder will be provided by:

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;
    }
}

Here's how the binder would be registered:

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());
});

I'm not quite sure if the new binder must be registered exactly at the same index, you can experiment with this.

And, at the end, this is how you can use it:

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
}

Model class is created by the binder which supplies the (already registered) service, and the rest of the model binders provide the property values from their usual sources.

HTH

查看更多
家丑人穷心不美
4楼-- · 2020-02-27 11:46

As I already explained in a comment, when creating an object using new, there is nothing from the dependency injection framework that is involved in the process. As such, it’s impossible for the DI framework to magically inject things into that object, it simply doesn’t know about it.

Since it does not make any sense to let the DI framework create your model instances (models are not a dependency), you will have to pass in your dependencies explicitly if you want the model to have them. How you do that depends a bit on what your models are used for, and what those dependencies are.

The simple and clear case would be to just have your model expect the dependencies on the constructor. That way, it is a compile time error if you do not provide them, and the model has access to them right away. As such, whatever is above, creating the models, is required to have the dependencies the model type needs. But at that level, it’s likely that this is a service or a controller which has access to DI and can request the dependency itself.

Of course, depending on the number of dependencies, this might become a bit complicated as you need to pass them all to the constructor. So one alternative would be to have some “model factory” that takes care of creating the model object. Another alternative would also be to use the service locator pattern, passing the IServiceCollection to the model which can then request whatever dependencies it needs. Note that is generally a bad practice and not really inversion of control anymore.

Both these ideas have the issue that they modify the way the object is created. And some models, especially those handled by Entity Framework, need an empty constructor in order for EF to be able to create the object. So at that point you will probably end up with some cases where the dependencies of your model are not resolved (and you have no easy way of telling).

A generally better way, which is also a lot more explicit, would be to pass in the dependency where you need it, e.g. if you have some method on the model that calculates some stuff but requires some configuration, let the method require that configuration. This also makes the methods easier to test.

Another solution would be to move the logic out of the model. For example the ASP.NET Identity models are really dumb. They don’t do anything. All the logic is done in the UserStore which is a service and as such can have service dependencies.

查看更多
狗以群分
5楼-- · 2020-02-27 11:47

I know my answer is late and may not exactly what you're asking for, but I wanted to share how I do it.

First of all: If you want to have a static class that resolves your dependencies this is a ServiceLocator and it's Antipattern so try not to use it as you can. In my case I needed it to call MediatR inside of my DomainModel to implement the DomainEvents logic.

Anyway, I had to find a way to call a static class in my DomainModel to get an instance of some registered service from DI.

So I've decided to use the HttpContext to access the IServiceProvider but I needed to access it from a static method without mention it in my domain model.

Let's do it:

1- I've created an interface to wrap the IServiceProvider

public interface IServiceProviderProxy
{
    T GetService<T>();
    IEnumerable<T> GetServices<T>();
    object GetService(Type type);
    IEnumerable<object> GetServices(Type type);
}

2- Then I've created a static class to be my ServiceLocator access point

public static class ServiceLocator
{
    private static IServiceProviderProxy diProxy;

    public static IServiceProviderProxy ServiceProvider => diProxy ?? throw new Exception("You should Initialize the ServiceProvider before using it.");

    public static void Initialize(IServiceProviderProxy proxy)
    {
        diProxy = proxy;
    }
}

3- I've created an implementation for the IServiceProviderProxy which use internally the IHttpContextAccessor

public class HttpContextServiceProviderProxy : IServiceProviderProxy
{
    private readonly IHttpContextAccessor contextAccessor;

    public HttpContextServiceProviderProxy(IHttpContextAccessor contextAccessor)
    {
        this.contextAccessor = contextAccessor;
    }

    public T GetService<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetService<T>();
    }

    public IEnumerable<T> GetServices<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetServices<T>();
    }

    public object GetService(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetService(type);
    }

    public IEnumerable<object> GetServices(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetServices(type);
    }
}

4- I should register the IServiceProviderProxy in the DI like this

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
    services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
    .......
}

5- Final step is to initialize the ServiceLocator with an instance of IServiceProviderProxy at the Application startup

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
    ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}

As a result now you can call the ServiceLocator in your DomainModel classes "Or and needed place" and resolve the dependencies that you need.

public class FakeModel
{
    public FakeModel(Guid id, string value)
    {
        Id = id;
        Value = value;
    }

    public Guid Id { get; }
    public string Value { get; private set; }

    public async Task UpdateAsync(string value)
    {
        Value = value;
        var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
        await mediator.Send(new FakeModelUpdated(this));
    }
}
查看更多
Ridiculous、
6楼-- · 2020-02-27 11:54

Is there another way than constructor injection to inject dependencies, and how?

The answer is "no", this cannot be done with "dependency injection". But, "yes" you can use the "service locator pattern" to achieve your end-goal.

You can use the code below to resolve a dependency without the use of constructor injection or the FromServices attribute. Additionally you can new up an instance of the class as you see fit and it will still work -- assuming that you have added the dependency in the Startup.cs.

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

The CallContextServiceLocator.Locator.ServiceProvider is the global service provider, where everything lives. It is not really advised to use this. But if you have no other choice you can. It would be recommended to instead use DI all the way and never manually instantiate an object, i.e.; avoid new.

查看更多
倾城 Initia
7楼-- · 2020-02-27 11:56

You can do it, check out [InjectionMethod] and container.BuildUp(instance);

Example:

Typical DI constructor (NOT NEEDED IF YOU USE InjectionMethod) public ClassConstructor(DeviceHead pDeviceHead) { this.DeviceHead = pDeviceHead; }

This attribute causes this method to be called to setup DI. [InjectionMethod] public void Initialize(DeviceHead pDeviceHead) { this.DeviceHead = pDeviceHead; }

查看更多
登录 后发表回答