Skip to content

Add support for [FromServices] attribute #22

@dotnetjunkie

Description

@dotnetjunkie

ASP.NET Core supports method injection in its MVC controllers through the use of the FromServices attribute. Simple Injector does not support this, and there is currently no intention in supporting this. This issue is merely a description on what it takes to add support for this in the ASP.NET Core integration package.

Things to take into consideration:

  • The default ASP.NET Core MVC implementation is strongly coupled to the built-in DI Container. This means that a [FromServices]-marked method argument, will always be resolved from the ASP.NET Core configuration system; not from Simple Injector.
  • When this feature gets implemented, it should be integrated into the Diagnostic subsystem. In other words, the container should be told that an [FromServices]-marked method argument is a KnownDependency of the specific controller. This allows object graphs to be visualized that include those services and analysis to be performed based on the relationship of the method-injected dependency with its consuming controller.
  • Due to the nature of how MVC works (with its model binder providers) and how method injection works, it seems hard to apply context-based injection on such dependency, e.g. An Index([FromServices]ILogger logger) method on an HomeController, whould ideally get an Logger<HomeController> injected in case the user uses Simple Injector's .AddLogging() extension method. This might not be possible, but further investigation is required.
  • The Potential Single Responsibility Violation message would likely be generated when a user starts using the [FromServices] attribute. A decision needs to be made whether counting of dependencies should be limited to constructors or not. If so, a change to the core library is required, separating dependency types (e.g. constructor, property, method, other).
  • When a [FromServices] dependency is not registered, verification should fail.

ASP.NET Core will, by default, resolve a [FromServices] dependency from the built-in configuration system—not from Simple Injector. These calls can be intercepted by replacing ASP.NET Core's Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ServicesModelBinderProvider. A possible implementation might look as follows:

public class SimpleInjectorServicesModelBinderProvider : IModelBinderProvider
{
    private readonly SimpleInjectorServicesModelBinder modelBinder;

    public SimpleInjectorServicesModelBinderProvider(Container container)
    {
        this.modelBinder = new SimpleInjectorServicesModelBinder(container);
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.BindingInfo.BindingSource != null &&
            context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Services))
        {
            return modelBinder;
        }

        return null;
    }

    private sealed class SimpleInjectorServicesModelBinder : IModelBinder
    {
        private readonly Container container;

        public SimpleInjectorServicesModelBinder(Container container)
        {
            this.container = container;
        }

        /// <inheritdoc />
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var model = this.container.GetInstance(bindingContext.ModelType);

            bindingContext.ValidationState.Add(
                model, new ValidationStateEntry() { SuppressValidation = true });

            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }
    }
}

This custom binder provider can replace the built-in one:

mvcOptions.ModelBinderProviders.RemoveType<ServicesModelBinderProvider>();
mvcOptions.ModelBinderProviders.Add(new SimpleInjectorServicesModelBinderProvider(container));

Limitations of this implementation:

  • No contextual injection; method-injected dependencies are considered to be root types.

This is part of the solution. What's still missing here is the integration with the diagnostics subsystem. This is likely something that should be done inside the AddControllerActivation() extension method. This likely involves the registration of a ExpressionBuilding or ExpressionBuilt event, because this is the interception point that allows informing Simple Injector about known dependencies.

Resources:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions