Accessing MVC DI Service inside other services

2019-07-11 01:44发布

问题:

I am constructing a HealthAPI Class Library Which provides a list of statistics to our HealthMonitor Service.

I have successfully got this working, The Middleware is recording Service boot time and recording response times, our health monitor is able to parse these values via a call to a our StatusController which has a number of actions returning IActionResult JSON responses.

We intend to reuse this over all of our services so have opted to keep the API controller within the Class Library along with the DI Service and middleware, to make the Controller accessable I originally did the following.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddApplicationPart(Assembly.Load(new AssemblyName("HealthApiLibrary"))); //Bring in the Controller for HealthAPI;
    services.AddSingleton<HealthApiService>();
}

However at the refactoring stage I want to clean this up a little by doing the following:

1) Refactor services.AddSingleton<HealthApiService>(); into services.AddHealthApi(); (Which we have not done any work towards just yet, but still may be relevent when answering this question)

2) Load in my StatusController as part of the services.AddHealthApi(); call.

I have tried the following:

public class HealthApiService
{
    public HealthApiService(IMvcBuilder mvcBuilder)
    {
        mvcBuilder.AddApplicationPart(Assembly.Load(new AssemblyName("HealthApiLibrary"))); //Bring in the Controller for HealthAPI

        ResponseTimeRecords = new Dictionary<DateTime, int>();
        ServiceBootTime = DateTime.Now;
    }

    public DateTime ServiceBootTime { get; set; }
    public Dictionary<DateTime,int> ResponseTimeRecords { get; set; }
    public string ApplicationId { get; set; }
}

however this just generates the following error:

InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.DependencyInjection.IMvcBuilder' while attempting to activate 'HealthApiLibrary.Services.HealthApiService'.

回答1:

1. Dependency injection

You get the exception because there is no IMvcBuilder registered in the service collection. I would not make sense to add this type to the collection as it is only used during the startup.

2. Extension method

You could create an extension method to achieve the method you wanted.

public static class AddHealthApiExtensions
{
    public static void AddHealthApi(this IServiceCollection services)
    {
        services.AddSingleton<HealthApiService>();
    }
}

3. Assembly.Load

Look at the comment from @Tseng.



回答2:

From what I gather, you are trying to allow the end user to supply their own dependencies to your HealthApiService. This is typically done using an extension method and one or more builder patterns. It is not a DI problem, but an application composition problem.

Assuming HealthApiService has 2 dependencies, IFoo and IBar, and you want users to be able to supply their own implementation for each:

public class HealthApiService : IHealthApiService
{
    public HealthApiService(IFoo foo, IBar bar)
    {

    }
}

Extension Method

The extension method has one overload for the default dependencies and one for any custom dependencies.

public static class ServiceCollectionExtensions
{
    public static void AddHealthApi(this IServiceCollection services, Func<HealthApiServiceBuilder, HealthApiServiceBuilder> expression)
    {
        if (services == null)
            throw new ArgumentNullException(nameof(services));
        if (expression == null)
            throw new ArgumentNullException(nameof(expression));

        var starter = new HealthApiServiceBuilder();
        var builder = expression(starter);
        services.AddSingleton<IHealthApiService>(builder.Build());
    }

    public static void AddHealthApi(this IServiceCollection services)
    {
        AddHealthApi(services, builder => { return builder; });
    }
}

Builder

The builder is what helps construct the HealthApiService one dependency at a time. It collects the dependencies and then at the end of the process the Build() method creates the instance.

public class HealthApiServiceBuilder
{
    private readonly IFoo foo;
    private readonly IBar bar;

    public HealthApiServiceBuilder()
        // These are the default dependencies that can be overridden 
        // individually by the builder
        : this(new DefaultFoo(), new DefaultBar()) 
    { }

    internal HealthApiServiceBuilder(IFoo foo, IBar bar)
    {
        if (foo == null)
            throw new ArgumentNullException(nameof(foo));
        if (bar == null)
            throw new ArgumentNullException(nameof(bar));
        this.foo = foo;
        this.bar = bar;
    }

    public HealthApiServiceBuilder WithFoo(IFoo foo)
    {
        return new HealthApiServiceBuilder(foo, this.bar);
    }

    public HealthApiServiceBuilder WithBar(IBar bar)
    {
        return new HealthApiServiceBuilder(this.foo, bar);
    }

    public HealthApiService Build()
    {
        return new HealthApiService(this.foo, this.bar);
    }
}

Usage

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // Default dependencies
        services.AddHealthApi();

        // Custom dependencies
        //services.AddHealthApi(healthApi => 
        //    healthApi.WithFoo(new MyFoo()).WithBar(new MyBar()));
    }

Bonus

If your default IFoo or IBar implementations have dependencies, you can make a builder class for each one. For example, if IFoo has a dependency IFooey you can create a builder for the default IFoo implementation, then overload the HealthApiServiceBuilder.WithFoo method with an expression:

public HealthApiServiceBuilder WithFoo(IFoo foo)
{
    return new HealthApiServiceBuilder(foo, this.bar);
}

public HealthApiServiceBuilder WithFoo(Func<FooBuilder, FooBuilder> expression)
{
    var starter = new FooBuilder();
    var builder = expression(starter);
    return new HealthApiServiceBuilder(builder.Build(), this.bar);
}

This can then be used like

services.AddHealthApi(healthApi => 
    healthApi.WithFoo(foo => foo.WithFooey(new MyFooey)));

More

Any other services (for example, controllers) that you need to register at application startup that you don't want the end user to interact with can be done inside of the extension method.

Reference

DI Friendly Library by Mark Seemann