Registering Services

Mapping interfaces to implementations, factory and instance registrations, TryAdd, keyed services and many implementations.

SyntaxIServiceCollection 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 for INotifier, build an EmailNotifier. The container fills that class's own constructor parameters too.
  • Factory. AddSingleton<INotifier>(sp => ...) runs your code to produce the instance. The sp parameter is an IServiceProvider, 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

Example · csharp
// 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: sms

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

Example · csharp
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.

Example · csharp
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.

Example · csharp
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

  • Be the first to comment on this lesson.