.NET
04 / 08

Dependency Injection & Middleware

.NET: Dependency Injection & Middleware

Dependency Injection

ASP.NET Core has a built-in DI container. Services are registered in Program.cs and injected via constructor injection.

// Service lifetimes
builder.Services.AddTransient<IEmailService, EmailService>();  // new instance every time
builder.Services.AddScoped<IUserRepository, UserRepository>();  // once per HTTP request
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();  // one instance total

// Interface + implementation pattern
public interface IUserService
{
    Task<UserDto?> GetByIdAsync(int id);
    Task<UserDto> CreateAsync(CreateUserDto dto);
}

public class UserService : IUserService
{
    private readonly IUserRepository _repo;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repo, ILogger<UserService> logger)
    {
        _repo = repo;
        _logger = logger;
    }

    public async Task<UserDto?> GetByIdAsync(int id)
    {
        _logger.LogInformation("Fetching user {UserId}", id);
        var user = await _repo.GetByIdAsync(id);
        return user is null ? null : new UserDto(user.Id, user.Name, user.Email, user.CreatedAt);
    }
}

// Options pattern — strongly typed config
public class JwtOptions
{
    public const string Section = "Jwt";
    public required string Secret { get; init; }
    public required string Issuer { get; init; }
    public int ExpiryMinutes { get; init; } = 60;
}

builder.Services.AddOptions<JwtOptions>()
    .BindConfiguration(JwtOptions.Section)
    .ValidateDataAnnotations();

// Inject options
public class TokenService(IOptions<JwtOptions> opts) {
    private readonly JwtOptions _opts = opts.Value;
}

Middleware Pipeline

// Middleware runs in registration order (request) then reverse order (response)
// Program.cs
app.UseExceptionHandler("/error");   // catch unhandled exceptions
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("AllowFrontend");
app.UseAuthentication();            // must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();               // or app.MapEndpoints()

// Custom middleware
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        _logger.LogInformation("{Method} {Path}", context.Request.Method, context.Request.Path);

        await _next(context);

        sw.Stop();
        _logger.LogInformation("Response {StatusCode} in {ElapsedMs}ms",
            context.Response.StatusCode, sw.ElapsedMilliseconds);
    }
}

// Register
app.UseMiddleware<RequestLoggingMiddleware>();

// Inline middleware (simpler for one-liners)
app.Use(async (context, next) => {
    context.Response.Headers["X-Frame-Options"] = "DENY";
    await next();
});

Configuration & Secrets

// appsettings.json — checked into git
{
  "Logging": { "LogLevel": { "Default": "Information" } },
  "AllowedHosts": "*",
  "Jwt": { "Issuer": "myapp", "ExpiryMinutes": 60 }
}

// appsettings.Development.json — overrides for dev
// appsettings.Production.json — overrides for prod (don't commit secrets)

// Environment variables override all (for production secrets)
// Hierarchy: appsettings < appsettings.{env} < env vars < command line

// User secrets (dev only — stored outside repo)
// dotnet user-secrets init
// dotnet user-secrets set "Jwt:Secret" "my-dev-secret-key-32chars"

// Access in code
var secret = builder.Configuration["Jwt:Secret"];
// or strongly typed:
var jwtOpts = builder.Configuration.GetSection("Jwt").Get<JwtOptions>();

Logging & Health Checks

// Structured logging with ILogger (Serilog recommended for production)
// dotnet add package Serilog.AspNetCore
builder.Host.UseSerilog((context, config) => {
    config
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext()
        .WriteTo.Console()
        .WriteTo.Seq("http://localhost:5341");
});

// In services — structured logging with parameters
_logger.LogWarning("User {UserId} exceeded rate limit: {Requests} requests", userId, count);
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);

// Health checks
builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "postgres")
    .AddRedis(redisConnection, name: "redis")
    .AddCheck("disk", () =>
        DriveInfo.GetDrives().Any(d => d.AvailableFreeSpace < 100_000_000)
            ? HealthCheckResult.Degraded("Low disk space")
            : HealthCheckResult.Healthy());

app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions {
    Predicate = check => check.Tags.Contains("ready")
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free