Dependency Injection
.NET ships a first-class dependency injection container (Microsoft.Extensions.DependencyInjection). Register services in Program.cs by lifetime, inject via constructor, resolve via the framework. The pattern scales from a console app to ASP.NET Core, gRPC, and worker services — same APIs, same mental model.
Lifetimes, registration, options, factory functions
EXAMPLE
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
// 1) Service interfaces
public interface IClock { DateTime UtcNow { get; } }
public interface IMailer { Task SendAsync(string to, string subject, string body, CancellationToken ct = default); }
public interface IOrderRepository { Task<string> CreateAsync(string customer, long totalCents, CancellationToken ct); }
public class SystemClock : IClock { public DateTime UtcNow => DateTime.UtcNow; }
public class SmtpMailer : IMailer
{
private readonly SmtpOptions _opts;
private readonly ILogger<SmtpMailer> _log;
public SmtpMailer(IOptions<SmtpOptions> opts, ILogger<SmtpMailer> log)
{
_opts = opts.Value;
_log = log;
}
public Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
{
_log.LogInformation("smtp send to={To} subject={Subject}", to, subject);
return Task.CompletedTask;
}
}
public class SmtpOptions { public string Host { get; set; } = ""; public int Port { get; set; } = 587; }
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IMailer _mailer;
private readonly IClock _clock;
public OrderService(IOrderRepository repo, IMailer mailer, IClock clock)
{ _repo = repo; _mailer = mailer; _clock = clock; }
public async Task<string> PlaceAsync(string customer, long totalCents, CancellationToken ct)
{
var id = await _repo.CreateAsync(customer, totalCents, ct);
await _mailer.SendAsync(customer, "Order placed", id, ct);
return id;
}
}
public class InMemoryOrderRepository : IOrderRepository
{
private long _next;
public Task<string> CreateAsync(string customer, long totalCents, CancellationToken ct)
=> Task.FromResult($"o{Interlocked.Increment(ref _next)}");
}
// 2) Composition root — register lifetimes deliberately
public static class Program
{
public static async Task Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Services
// Singleton: one per process. Cheap, stateless, thread-safe.
.AddSingleton<IClock, SystemClock>()
// Scoped: one per request (or per CreateScope). Best for per-request state.
.AddScoped<IOrderRepository, InMemoryOrderRepository>()
.AddScoped<OrderService>()
// Transient: a new instance every time. Cheap, stateless.
.AddTransient<IMailer, SmtpMailer>();
// 3) Options pattern — strongly typed config
builder.Services
.AddOptions<SmtpOptions>()
.Bind(builder.Configuration.GetSection("Smtp"))
.ValidateDataAnnotations()
.ValidateOnStart();
// 4) Factory registrations — when the constructor needs runtime values
builder.Services.AddSingleton<Func<string, IMailer>>(sp => key =>
key == "sms"
? sp.GetRequiredService<TwilioMailer>()
: sp.GetRequiredService<SmtpMailer>());
// 5) Logging is registered by Host.CreateApplicationBuilder
builder.Services.AddLogging();
using var host = builder.Build();
// 6) Resolve at the composition root, not deep in the call stack
using (var scope = host.Services.CreateScope())
{
var orders = scope.ServiceProvider.GetRequiredService<OrderService>();
var id = await orders.PlaceAsync("alice@example.com", 4995, CancellationToken.None);
Console.WriteLine($"placed {id}");
}
}
}
// ============================================================
// Decision matrix
// ============================================================
// - Stateless + thread-safe? -> Singleton
// - Per-request state (Db context, user)? -> Scoped
// - Tiny + frequently created? -> Transient
// - Need to choose at runtime? -> Factory function
//
// Anti-patterns
// - Storing scoped service in a singleton (captured DbContext = leaks)
// - new SomeService() inside a controller (DI bypass — hides dependency)
// - Service Locator (sp.GetRequiredService deep in business logic) — pass in instead
public class TwilioMailer : IMailer
{
public Task SendAsync(string to, string subject, string body, CancellationToken ct = default) => Task.CompletedTask;
}
Why it matters
Lifetimes are the trap: a Scoped service captured by a Singleton (e.g., a singleton EventBus that holds a reference to a DbContext) lives forever, even though the DbContext expected to die with the request. Use IServiceScopeFactory to create a fresh scope when a singleton needs scoped services, never inject scoped into singleton constructors.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
builder.Services.AddSingleton<IClock, SystemClock>(); builder.Services.AddScoped<IUserRepo, SqlUserRepo>();Try it Yourself »
Discussion
Loading…