Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inject dependencies into models in asp.net core?

Let's say I have a controller action that looks like this:

[HttpPost]
public async Task<IActionResult> Add([FromBody] MyModel model){
    await model.Save();
    return CreatedAtRoute("GetModel", new {id = model.Id}, model);
}

In order to get model.Save to work, it needs some dependencies:

public class MyModel{
    private readonly ApplicationDbContext _context;
    public MyModel(ApplicationDbContext context){
        _context = context;
    }
    public async Task Save(){
        // Do something with _context;
    }
}

As of right now, context is null in MyModel's constructor. How can I inject it? I'm aware that I can inject services into the controller and perform operations on my model that way, but what if I'd rather use an object-oriented approach than an anemic domain model? Is it simply impossible?

like image 878
Eric B Avatar asked Sep 01 '16 16:09

Eric B


People also ask

How can we inject the service dependency into the controller in ASP.NET Core?

With an ASP. NET Core MVC application, we can use a parameter in the controller's constructor. We can add the service type and it will automatically resolve it to the instance from the DI Container. Once we have done that, we can assign the instance to a field in our controller.

How are dependencies injected into an object?

The injector class injects dependencies broadly in three ways: through a constructor, through a property, or through a method. Constructor Injection: In the constructor injection, the injector supplies the service (dependency) through the client class constructor.

How do you inject DbContext in dependency injection for ASP.NET Core?

DbContext in dependency injection for ASP.NET CoreThe context is configured to use the SQL Server database provider and will read the connection string from ASP.NET Core configuration. It typically does not matter where in ConfigureServices the call to AddDbContext is made.


2 Answers

You should consider to refactor code using DTO (Data Transfer Object) pattern. If simply

  • MyModel should only by a data container -> contains properties/calculated properties.
  • logic from Save() method should be extracted into separate class like ModelRepository, that should know about dependencies like ApplicationDbContext:

     public class ModelRepository
     {
        private readonly ApplicationDbContext _context;
    
        public ModelRepository(ApplicationDbContext context)
        {
            _context = context;
        }
    
        public async Task Save(MyModel model)
        {
            // Do something with _context;
        }
    }
    
  • finally, your controller should use instance of ModelRepository (resolve it using build-in DI) to save your data:

    [HttpPost]
    public async Task<IActionResult> Add([FromBody] MyModel model)
    {
        await _modelRepository.Save(model);
        return CreatedAtRoute("GetModel", new {id = model.Id}, model);
    }
    
like image 95
Set Avatar answered Nov 04 '22 02:11

Set


Well, besides DTO you may also use rich models. You'll need a customized model binder, which will take care of ctor injection into the model.

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

like image 5
Alexander Christov Avatar answered Nov 04 '22 02:11

Alexander Christov