ASP.Net Core Dependency Injection
See docs.asp.net
There is a simple DI built into core (using IServiceProvider, which goes back to .net 1.1 but now is in Microsoft.Extensions.DependencyInjection.Abstractions).
It's functional enough that a full DI container isn't required, but the usual suspects have hooks.
Configuration
Services are configured in Startup.ConfigureServices(IServiceCollection services)
Most middleware services are added using the standard services.AddX convention.
public void ConfigureServices(IServiceCollection services)
{
// Add middleware services.
services.AddMvc();
// Add application services.
//newly created each time
services.AddTransient<IService, Service>();
//created for request, reused within request
services.AddScoped<IService, Service>();
//create a singleton, created here
services.AddSingleton<IService>(new Service());
//create a singleton, created lazily
services.AddSingleton<IService, Service>();
}
Use
Consume using constructor injection- chained dependencies are fine.
If absolutely necessary to break DI, you can also access Service-Locator style, from HttpContext.RequestServices
Consoles
You can use something similar in consoles, but it's overkill when you just want simple configuration, DI and logging. It's best to call the Host.CreateDefaultBuilder, grab the IServiceProvider from it, and GetRequiredService of your class, which is now fully DI configured for you.
This sample uses NLog. If you have disposables in your services, you'll need to cast the service as IDisposable within a using block.
class Program
{
static async Task Main(string[] args)
{
var logger = LogManager.GetCurrentClassLogger();
try
{
var host = CreateHostBuilder(args).Build();
//we don't use the host, just grab the DI container which has been built with options
var serviceProvider = host.Services;
var runner = serviceProvider.GetRequiredService<Runner>();
runner.Run();
}
catch (Exception ex)
{
// NLog: catch any exception and log it.
logger.Error(ex, "Top level exception");
throw;
}
finally
{
// Flush and stop internal timers/threads
LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.Configure<ConfigDetails>(hostContext.Configuration.GetSection("ConfigDetails"));
services.AddTransient<Runner>();
services.AddLogging(loggingBuilder =>
{
// configure Logging with NLog
loggingBuilder.ClearProviders();
loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
loggingBuilder.AddNLog();
});
});
}
Autofac
Eg Autofac, add Autofac.Extensions.DependencyInjection and in Program.cs:
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory()) //etc
Fuller example
// Add services to the container.
//grab the connection string
var connString = builder.Configuration.GetConnectionString("Database");
//autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(cb =>
{
cb.RegisterModule(new RegistryModule());
//inject the connection string
cb.Register(c => new ConnectionFactory(connString)).As<IConnectionFactory>();
});
Extract the DI config itself into an autofac module. Especially if you have multiple websites/apis/consoles/tests, one or more DI config modules is the best way to build them in the same way.
namespace MyWebApi;
public class RegistryModule : Module
{
protected override void Load(ContainerBuilder builder)
{
//hook up all your assemblies
builder.RegisterAssemblyTypes(typeof(MyService).Assembly).PublicOnly().AsImplementedInterfaces();
}
}
DI with user
The key here is to hook into IHttpContextAccessor in a lifetime scope.
{
ClaimsPrincipal User { get; }
}
public class UserContext : IUserContext
{
public UserContext(IHttpContextAccessor httpContextAccessor)
{
User = httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal();
}
public ClaimsPrincipal User { get; }
}
using Autofac;
using Logic;
using Microsoft.AspNetCore.Http;
namespace Api.Startup
{
public class RegistryModule : Module
{
protected override void Load(ContainerBuilder builder)
{
//register interfaces
builder.RegisterAssemblyTypes(typeof(ConnectionFactory).Assembly).PublicOnly().AsImplementedInterfaces();
//register also concrete classes
builder.RegisterAssemblyTypes(typeof(ConnectionFactory).Assembly).PublicOnly();
//register httpContext (yes, as a singleton)
builder.RegisterType<HttpContextAccessor>().As<IHttpContextAccessor>().SingleInstance();
//and the user is scoped
builder.RegisterType<UserContext>().As<IUserContext>().InstancePerLifetimeScope();
}
}
}
And hook up the module from your Program.Main
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
//grab the connection string
var connectionString = builder.Configuration.GetConnectionString("Db");
//autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(cb =>
{
cb.RegisterModule(new RegistryModule());
//inject the connection string
cb.Register(c => new ConnectionFactory(connectionString)).As<IConnectionFactory>();
});
Testing DI
You should have some sort of test for UI, plus, except for simple unit tests, you probably want DI in tests.
- Install-Package Autofac
- Install-Package Microsoft.Extensions.Hosting so we can grab configuration (or use Microsoft.Extensions.Configuration for more low-level control)
Here's the test structure we want (this is an integration test - we save and read a record but the non-committing transaction ensures nothing remains in the db).
using Services;
using Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Transactions;
namespace IntegrationTests
{
[TestClass]
public class TestService
{
private readonly TestContainer _container = new TestContainerBuilder().Build();
[TestMethod]
public void TestSave()
{
using (var scope = _container.LifetimeScope)
{
var service = scope.Resolve<Service>();
using (new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
var model = new Model();
var result = service.Save(model);
Assert.IsTrue(result.IsSuccess);
var dbModel = service.Read(result.Id);
Assert.IsNotNull(dbModel);
}
}
}
We have a TestContainer to wrap the DI container
{
private readonly IContainer _container;
public TestContainer(IContainer container)
{
_container = container;
}
public ILifetimeScope? LifetimeScope => _container.BeginLifetimeScope();
public void Dispose()
{
_container.Dispose();
}
}
This is setup in a builder.
- We use Microsoft.Extensions.Hosting to build configuration from appsettings.json etc (remember to make it Copy Always)
- A built Host doesn't expose Configuration, just Services, so we grab the reference during ConfigureServices. Ignore the nullable warnings.
- For injecting a connection string, we have a I/ConnectionFactory which holds it.
- If you have Automapper or other dependencies, you can initialise them here
- We're using Autofac modules, so various projects can re-use mostly the same DI config. If you inject users see below for test specific code
using Services.AutoMapper;
using DataAccess;
using Models;
using Microsoft.Extensions.Caching.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace IntegrationTests
{
public class TestContainerBuilder
{
public TestContainer Build()
{
//if you use automapper, call the MapperConfiguration.CreateMapper()
AutoMap.RegisterMappings();
//read the configuration (appsettings.json)
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
Configuration = context.Configuration;
})
.Build();
#pragma warning disable CS8604 // Possible null reference argument.
var connectionString = Configuration.GetConnectionString("Db");
#pragma warning restore CS8604 // Possible null reference argument.
//build the Autofac container
var builder = new ContainerBuilder();
builder.Register(c => new ConnectionFactory(connectionString)).As<IConnectionFactory>();
builder.RegisterModule<TestRegistryModule>();
//Microsoft.Extensions.Caching.Memory added manually
builder.Register(m=> new MemoryCache(new MemoryCacheOptions())).As<IMemoryCache>().SingleInstance();
var container = builder.Build();
return new TestContainer(container);
}
public IConfiguration? Configuration { get; set; }
}
}
If you're injecting the user (see the IHttpContextAccessor trick above), you can create a special test user context (see IUserContext above). If you use an IClaimsTransformer in your website pipeline, you'll need to run that too.
using System;
using System.Collections.Generic;
using System.Security.Claims;
namespace IntegrationTests
{
//register this in DI
//builder.RegisterType<TestUserContext>().As<IUserContext>().InstancePerLifetimeScope();
public class TestUserContext : IUserContext
{
public TestUserContext()
{
//using the name of the person running the test, or a fixed string eg "TestUser"
var name = new Claim(ClaimTypes.Name, Environment.UserName);
var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { name }, "Basic"));
User = principal;
}
public ClaimsPrincipal User { get; }
}
}