2

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'.

tornup
  • 263
  • 1
  • 4
  • 15
  • 2
    Why do you even want to load it via `Assembly.Load`? Just put it in a nuget package, reference it, then call the extension method which registers the middleware. Controllers will be automatically detected by ASP.NET Core MVC as long as they inherit from Controller base class or reference the `Microsoft.AspNetCore.Mvc` package – Tseng Oct 06 '17 at 09:15
  • Accessing the container from within a service is the dreaded service locator anti-pattern. But after reading your question, that isn't even close to what you are trying to achieve. All you are really after is to create an extension method to make your library easier to register. It is that extension method that should register your services at application startup. – NightOwl888 Oct 06 '17 at 09:17
  • @Tseng, Could you provide a little more detail, I did a bit of testing as I was developing this and couldn't find any information on how the ASP.net web application project was able to access a controller located within the class library. – tornup Oct 06 '17 at 10:21
  • ASP.NET Core discovers controllers by convention. It looks in every assembly loaded/referenced by the main project. So if you have a class library with a controller **and** this class library with references to `Microsoft.AspNetCore.Mvc` / `Microsoft.AspNetCore.MvcCore`, it will look for all classes which either inherit from `Controller` base class or have `Controller` suffix (i.e. `MyController`) and register them as controllers. All you need to do is to reference the library – Tseng Oct 06 '17 at 10:27
  • From the question it's not clear if you attempt to use **two** DI containers (one in PCL, one in the main Application). If this is the case, don't do it. There should be only **a single container** per application which is configured in the composition root (in case of ASP.NET Core: `Startup` class of the main executable. PCLs don't have a `Startup` class). You then register your services with the main application – Tseng Oct 06 '17 at 10:28
  • I suspect I understand what is happening here, when I originally ran into this issue, I reached out to SO and found the following [answer](https://stackoverflow.com/a/40492488/603718) i think this problem is caused because i am using attribute routing? – tornup Oct 06 '17 at 10:31
  • 1
    Correct, you should not be using attribute routing with a library, you should be using convention-based routing. It's easier for you to tie it into the host application at application startup that way. – NightOwl888 Oct 06 '17 at 10:39

2 Answers2

2

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.

NtFreX
  • 10,379
  • 2
  • 43
  • 63
  • In your code Snippet for 2. Extension Method, it references `services` but this is not declared anyway. should the `IServiceCollection` be passed in somewhere? – tornup Oct 06 '17 at 10:27
  • 1
    @tornup I mistaken `IMvcBuilder` for `IServiceCollection`. I've changed my answer. To see how the builder pattern can be applied you can take a look at @NightOwl888 answer. – NtFreX Oct 06 '17 at 11:33
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

NightOwl888
  • 55,572
  • 24
  • 139
  • 212