0

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://learn.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.

Mikkel
  • 156
  • 1
  • 15

2 Answers2

0

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
     ```
Mikkel
  • 156
  • 1
  • 15
-1

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.

Community
  • 1
  • 1
weichch
  • 9,306
  • 1
  • 13
  • 25
  • Thank you for the answer weichch. I tried your solution but the _innerBinder.BindModelAsync throws an error: System.InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder' while attempting to activate 'Project.Controllers.Controller.Requests.CustomBinders.DependencyInjectionModelBinder'. at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired) at lambda_method(Closure , IServiceProvider , Object[] ) .... – Mikkel Apr 06 '20 at 12:08
  • @Mikkel did you remember to register the implementation of IModelBinder in the ConfigureServices method in Startup.cs? – sunero4 Apr 06 '20 at 12:21
  • Weird. You shouldn’t need to resolve the custom model binder as it is created in the custom model binder provider in example. I’ll take a look when I have time. – weichch Apr 06 '20 at 12:27
  • @sunero4 I have added the binderprovider as shown by weichch but adding the specific implementation of DependencyInjectionModelBinder to DI cause a circular dependency exception. `services.AddTransient();` `Requests.CustomBinders.DependencyInjectionModelBinder': A circular dependency was detected for the service of type '` – Mikkel Apr 06 '20 at 12:42
  • @weichch my mistake I had added `[ModelBinder(BinderType = typeof(DependencyInjectionModelBinder)]` to the class I wanted "bound", and when it tried to use that as the model binder it couldn't find it and crashed. But leaving it out makes it work fine. Sorry for the confusion on my part. However I don't quite know how I can do "property injection" in the // Do DI stuff section. – Mikkel Apr 06 '20 at 13:15
  • Do you have only one model needing this and content is json? And are you using `Newtwonsoft.Json`? If so, you could do a `ContractResolver`. That'd be much simpler. See updated answer. – weichch Apr 06 '20 at 21:27
  • Thank you for the help weichch, but I think I will try something else than having the model/logic in the same class like this. I have read up on it and it seems like it is not easily supported for a reason. After having gained some new vocabulary from your answer I was able to learn some more. I believe that your soloution with Json.net might have been enough for me, but I use the new System.text json serialization with .net core 3. – Mikkel Apr 07 '20 at 09:53