iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…