简体   繁体   中英

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?

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

您是否尝试在configure服务方法中添加服务以提供服务

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM