.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