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?
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.
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.
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.
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);
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With