简体   繁体   中英

ASP.net core 3.1, add dependency injection support for controller actions auto bound [FromBody] parameters

I know how to inject into controller actions and the controller directly, by adding the service to the IServiceprovider and then the framework just handles it for me and in the case of controller actions I could add [Microsoft.AspNetCore.Mvc.FromServices] and it would inject the service to the specific action.

But that requires my controller to know specifically what the underlying parameter would need, which is a coupling I find unnecessary and potentially harmful.

I would like to know if it is possible to have something close to the following:

[HttpPost]
public async Task<ActionResult> PostThings([FromBody]ParameterClassWithInjection parameter) {
  parameter.DoStuff();
...}

public class ParameterClassWithInjection{
  public readonly MyService _myService;
  public ParameterClassWithInjection(IMyService service){ _myService = service;}

  public void DoStuff(){ _myService.DoStuff(); }
}

I have only found something about a custom model binder in the docs. https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-3.1#custom-model-binder-sample

This shows how you can create a custom binder and have a custom provider supply the injection. It just seems I need to implement a lot of boilerplate code from the automatic binding (which works absolutely fine for me in every case) in order to get some dependency injection.

I would hope you could point me in a better direction or put my quest to a rest if this is the only option.

Shotcut

If the content type is JSON and you are using Newtonsoft.Json, you could deserialize your model with dependency injection using a custom contract resolver.

Model binding

Otherwise, if you need to support other content types etc, you need to go a complex way.

For specific model, or models only FromBody :

Use a model binder and do property injection. See my answer couple weeks ago.

For generic model or models from other sources:

You need a custom model binder provider. In the model binder provider, you could iterate through existing model binder providers, find the model binder for the current model binding context, then decorate it with your own model binder decorator which can do DI for models.

For example:

public class DependencyInjectionModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        // Get MVC options.
        var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>();

        // Find model binder provider for the current context.
        IModelBinder binder = null;
        foreach (var modelBinderProvider in mvcOptions.Value.ModelBinderProviders)
        {
            if (modelBinderProvider == this)
            {
                continue;
            }

            binder = modelBinderProvider.GetBinder(context);
            if (binder != null)
            {
                break;
            }
        }

        return binder == null
            ? null
            : new DependencyInjectionModelBinder(binder);
    }
}

// Model binder decorator.
public class DependencyInjectionModelBinder : IModelBinder
{
    private readonly IModelBinder _innerBinder;

    public DependencyInjectionModelBinder(IModelBinder innerBinder)
    {
        _innerBinder = innerBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        await _innerBinder.BindModelAsync(bindingContext);

        if (bindingContext.Result.IsModelSet)
        {
            var serviceProvider = bindingContext.HttpContext.RequestServices;

            // Do DI stuff.
        }
    }
}

// Register your model binder provider
services.AddControllers(opt =>
{
    opt.ModelBinderProviders.Insert(
        0, new DependencyInjectionModelBinderProvider());
});

This works for property injection.

For constructor injection, because creation of model still happens in the inner model binder, you definitely need more code than this example, and I haven't tried to get constructor injection working in this case.

I later chose to do the following.

[HttpPost]
public async Task<ActionResult> PostThings([FromBody]ParameterClassWithInjection parameter, [FromServices] MyService) {
await MyService.DoStuff(parameter);  
...}

Where the service is injected into the api action.

I then opted for having very small services, one per request to keep it very split up. If these services then needed some shared code, let's say from a repository then I simply injected that into these smaller services.

Upsides includes; it is very easy to mock and test in unit tests, and keeps changes simple without affecting other actions because it is very explicitly stated that this "service"/request is only used once.

Downside is that you have to create a lot of classes. With nice folder structuring you can mitigate some of the overview burden.

 - MyControllerFolder
    -Controller
    -Requests
     - MyFirstRequsetFolder
       - Parameter.cs
       - RequestService.cs
     ```

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