Registering Services
Mapping interfaces to implementations, factory and instance registrations, TryAdd, keyed services and many implementations.
IServiceCollection AddSingleton<TService, TImplementation>(this IServiceCollection services)Registering a service means telling the container two things: what callers will ask for, and how to produce it. Registrations go into an IServiceCollection, which in an ASP.NET Core app is builder.Services. When you call Build, that list becomes the container that answers requests.
The shapes of a registration
- Type to type.
AddSingleton<INotifier, EmailNotifier>()says: when someone asks forINotifier, build anEmailNotifier. The container fills that class's own constructor parameters too. - Factory.
AddSingleton<INotifier>(sp => ...)runs your code to produce the instance. Thespparameter is anIServiceProvider, so the factory can resolve other services. Use this when construction needs a value the container cannot supply. - Instance.
AddSingleton(new EmailNotifier())hands over an object you already made. The container will not dispose it for you.
Several implementations of one interface
Registering the same service type twice does not replace anything. Both registrations stay. Asking for INotifier returns the last one registered; asking for IEnumerable<INotifier> returns all of them in registration order. TryAddSingleton adds only if that service type has no registration yet, which is how libraries supply a default a host application can override.
Keyed services
A keyed registration attaches a label. AddKeyedSingleton<INotifier, SmsNotifier>("urgent") is found only by that key, through GetRequiredKeyedService or a [FromKeyedServices("urgent")] constructor parameter. Keyed registrations are invisible to a plain request for the type.
Example
// Package: Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
ServiceCollection services = new ServiceCollection();
// Map an interface to the class that implements it.
services.AddSingleton<INotifier, EmailNotifier>();
// A second implementation. Both registrations are kept, in this order.
services.AddSingleton<INotifier, SmsNotifier>();
// TryAdd adds nothing here: INotifier already has a registration.
services.TryAddSingleton<INotifier, EmailNotifier>();
// A keyed registration is found by its key, not by its type alone.
services.AddKeyedSingleton<INotifier, SmsNotifier>("urgent");
using ServiceProvider provider = services.BuildServiceProvider();
// The type alone gives the last non-keyed registration.
Console.WriteLine($"one: {provider.GetRequiredService<INotifier>().Name}");
// IEnumerable<T> gives every non-keyed registration, in registration order.
foreach (INotifier notifier in provider.GetServices<INotifier>())
{
Console.WriteLine($"all: {notifier.Name}");
}
Console.WriteLine($"keyed: {provider.GetRequiredKeyedService<INotifier>("urgent").Name}");
interface INotifier
{
string Name { get; }
}
class EmailNotifier : INotifier
{
public string Name => "email";
}
class SmsNotifier : INotifier
{
public string Name => "sms";
}
// Output:
// one: sms
// all: email
// all: sms
// keyed: smsWhen to use it
- Register a repository interface so controllers never name the concrete class.
- Ship a library default that an application can override without editing the library.
- Run every validation rule in an assembly without listing them in the calling class.
- Point one caller at the SMS notifier and everyone else at email.
More examples
Shorthand and longhand for the same registration
Every Add and TryAdd method is a wrapper that creates a ServiceDescriptor and puts it in the list. Seeing the long form explains why registrations behave like list entries rather than dictionary keys, and why Replace and RemoveAll exist. The long form is also what you write when the service type is only known at runtime, because it takes Type values rather than type arguments.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
ServiceCollection services = new ServiceCollection();
// Shorthand.
services.AddScoped<INotifier, EmailNotifier>();
// Longhand: exactly what the shorthand builds and adds.
services.Add(new ServiceDescriptor(
serviceType: typeof(INotifier),
implementationType: typeof(EmailNotifier),
lifetime: ServiceLifetime.Scoped));
// Same thing again through a static helper on ServiceDescriptor.
services.Add(ServiceDescriptor.Scoped<INotifier, EmailNotifier>());
// Replace removes the first registration of INotifier, then adds this one.
// Use it to override a default; Add would leave both in place.
services.Replace(ServiceDescriptor.Scoped<INotifier, SmsNotifier>());A factory registration that needs a value the container has not got
Reach for a factory when a constructor takes a plain string, number or flag alongside its services. A GET to /ping returns the text that Describe returns. Keep factories fast and synchronous: they run while a service is being resolved, and calling .Result on a Task inside one can deadlock the request.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// StorageClient needs a connection string and a logger. The container can
// supply the logger, so ask it for that and pass the string yourself.
builder.Services.AddSingleton<IStorageClient>(sp =>
{
IConfiguration config = sp.GetRequiredService<IConfiguration>();
ILogger<StorageClient> logger = sp.GetRequiredService<ILogger<StorageClient>>();
string connection = config["Storage:ConnectionString"] ?? "UseDevelopmentStorage=true";
return new StorageClient(connection, logger);
});
WebApplication app = builder.Build();
app.MapGet("/ping", (IStorageClient client) => client.Describe());
app.Run();Many implementations, and picking one by key
Asking for IEnumerable<T> is the standard way to build a pipeline or a rule set that grows by registration alone, with no switch statement to edit. Keyed services solve the opposite problem: several implementations exist and one caller must name the one it wants. Before keyed services arrived in .NET 8, that needed a hand-written factory interface, which is still common in older code.
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
// Every registered rule runs. Add a rule by adding one registration.
public class Validator(IEnumerable<IRule> rules)
{
public int RuleCount => rules is ICollection<IRule> c ? c.Count : 0;
}
// One specific implementation, named at the point of use.
public class UrgentSender([FromKeyedServices("urgent")] INotifier notifier)
{
public string Via => notifier.Name;
}
// builder.Services.AddSingleton<IRule, LengthRule>();
// builder.Services.AddSingleton<IRule, ProfanityRule>();
// builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("urgent");
// builder.Services.AddSingleton<Validator>();
// builder.Services.AddSingleton<UrgentSender>();
Discussion