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