
Microsoft Extensions
- 19 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
microsoft-extensions is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- microsoft-extensions
- AI & Agent Building
- AI-coding skill
Microsoft Extensions by the numbers
- 19 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,571 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill microsoft-extensionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Microsoft.Extensions for .NET
Trigger On
- wiring dependency injection, configuration, logging, or options
- introducing Generic Host patterns into non-web .NET apps
- cleaning up service registration, typed HTTP clients, or shared infrastructure code
Workflow
1. Prefer the Generic Host for apps that need configuration, DI, logging, hosted services, or coordinated startup. 2. Keep service registration predictable: composition at the edge, concrete implementations hidden behind interfaces only where that abstraction buys flexibility. 3. Use options binding for structured configuration and validate configuration at startup when bad settings would fail later at runtime. 4. Prefer IHttpClientFactory and typed or named clients for outbound HTTP instead of ad-hoc singleton or per-call HttpClient usage. 5. Use logging categories and config-driven log levels rather than scattered ad-hoc logging behavior. 6. For Kubernetes-hosted services, prefer the stable Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes package from the 10.7 line when CPU/memory request and limit metrics should come from pod resource quotas. 7. Avoid building mini-frameworks over Microsoft.Extensions unless the repo genuinely needs reusable composition primitives.
Deliver
- clean host wiring and service registration
- configuration and logging that are observable and testable
- infrastructure code that fits naturally with the .NET stack
Validate
- service lifetimes are correct
- configuration is strongly typed where it matters
- host setup remains easy to debug and reason about
- Kubernetes resource monitoring uses the stable quota-provider APIs when pod request/limit dimensions matter
References
- patterns.md - DI patterns, Configuration patterns, Options pattern, Logging patterns, HttpClientFactory patterns, Hosted Service patterns
- anti-patterns.md - Common mistakes with DI, configuration, options, logging, HttpClient, and hosted services
{
"version": "1.1.0",
"category": "Core",
"package_prefix": "Microsoft.Extensions"
}
Microsoft.Extensions Anti-Patterns
Dependency Injection Anti-Patterns
Service Locator Pattern
Problem: Resolving services manually instead of using constructor injection.
// BAD: Service locator
public class OrderService(IServiceProvider serviceProvider)
{
public async Task ProcessAsync(Order order)
{
// Hidden dependency, hard to test, breaks DI benefits
var repository = serviceProvider.GetRequiredService<IOrderRepository>();
await repository.SaveAsync(order);
}
}
// GOOD: Explicit constructor injection
public class OrderService(IOrderRepository repository)
{
public async Task ProcessAsync(Order order)
{
await repository.SaveAsync(order);
}
}Exception: IServiceProvider is acceptable in factory classes or hosted services that need to create scopes.
Captive Dependencies
Problem: A singleton service captures a scoped or transient dependency.
// BAD: Singleton captures scoped DbContext
services.AddSingleton<ICacheService, CacheService>();
services.AddDbContext<AppDbContext>(); // Scoped by default
public class CacheService(AppDbContext dbContext) : ICacheService
{
// dbContext is now captive - same instance for entire app lifetime
// This causes threading issues and stale data
}
// GOOD: Use IServiceScopeFactory for scoped dependencies
public class CacheService(IServiceScopeFactory scopeFactory) : ICacheService
{
public async Task RefreshAsync()
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Use dbContext within this scope
}
}Constructor Over-Injection
Problem: Too many dependencies indicate a class is doing too much.
// BAD: 8+ dependencies suggest SRP violation
public class OrderProcessor(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IInventoryService inventoryService,
IPaymentService paymentService,
IShippingService shippingService,
INotificationService notificationService,
ILogger<OrderProcessor> logger,
IOptions<OrderSettings> options,
IAuditService auditService,
IMetricsService metricsService)
{
// This class is doing too much
}
// GOOD: Split into focused services or use aggregates
public class OrderProcessor(
IOrderWorkflow workflow,
ILogger<OrderProcessor> logger,
IOptions<OrderSettings> options)
{
public async Task ProcessAsync(Order order)
{
await workflow.ExecuteAsync(order);
}
}Registering Implementations Instead of Interfaces
Problem: Registering concrete types makes testing and swapping implementations difficult.
// BAD: Hard to mock in tests
services.AddScoped<OrderService>();
// GOOD: Register interface to implementation
services.AddScoped<IOrderService, OrderService>();Inappropriate Lifetimes
Problem: Using wrong service lifetimes.
// BAD: Transient for expensive-to-create services
services.AddTransient<IExpensiveService, ExpensiveService>();
// BAD: Singleton for services with request-specific state
services.AddSingleton<IUserContext, UserContext>();
// GOOD: Match lifetime to actual requirements
services.AddSingleton<IExpensiveService, ExpensiveService>(); // Create once
services.AddScoped<IUserContext, UserContext>(); // Per-request state---
Configuration Anti-Patterns
Magic Strings Everywhere
Problem: Configuration keys scattered as strings throughout the code.
// BAD: Magic strings
var connectionString = configuration["ConnectionStrings:DefaultConnection"];
var timeout = int.Parse(configuration["HttpClient:Timeout"]);
// GOOD: Strongly typed configuration
public class HttpClientSettings
{
public const string SectionName = "HttpClient";
public int Timeout { get; init; } = 30;
}
services.Configure<HttpClientSettings>(configuration.GetSection(HttpClientSettings.SectionName));No Validation
Problem: Invalid configuration discovered at runtime instead of startup.
// BAD: Fails at runtime when service is first used
public class EmailService(IOptions<EmailSettings> options)
{
public void Send(string to, string subject, string body)
{
// Crashes here if SmtpHost is null
using var client = new SmtpClient(options.Value.SmtpHost);
}
}
// GOOD: Validate at startup
services.AddOptions<EmailSettings>()
.BindConfiguration("Email")
.ValidateDataAnnotations()
.ValidateOnStart();Secrets in Configuration Files
Problem: Sensitive data in appsettings.json checked into source control.
// BAD: appsettings.json in source control
{
"Database": {
"ConnectionString": "Server=prod;Password=actualPassword123"
}
}Solutions:
- Use User Secrets for local development
- Use environment variables for production
- Use Azure Key Vault, AWS Secrets Manager, or similar
- Use
appsettings.Development.json(gitignored) for local overrides
Ignoring Configuration Hierarchy
Problem: Not understanding that later sources override earlier ones.
// Configuration sources (later overrides earlier):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. Environment variables
// 4. Command-line arguments
// BAD: Adding JSON after environment variables reverses expected precedence
builder.Configuration.AddJsonFile("override.json"); // Now overrides env vars!
// GOOD: Understand and maintain intended precedence
builder.Configuration.AddJsonFile("defaults.json", optional: true);
// Environment variables and command-line args still win---
Options Pattern Anti-Patterns
Using IOptions When Reload Is Needed
Problem: IOptions<T> never reloads after startup.
// BAD: IOptions won't see configuration changes
public class FeatureService(IOptions<FeatureFlags> options)
{
public bool IsEnabled(string feature) =>
options.Value.EnabledFeatures.Contains(feature);
// This never updates even if config file changes
}
// GOOD: Use IOptionsMonitor for live updates
public class FeatureService(IOptionsMonitor<FeatureFlags> options)
{
public bool IsEnabled(string feature) =>
options.CurrentValue.EnabledFeatures.Contains(feature);
}Mutable Options Classes
Problem: Options objects that can be modified after binding.
// BAD: Mutable options
public class ApiSettings
{
public string BaseUrl { get; set; } = "";
public int Timeout { get; set; }
}
// GOOD: Immutable options with init-only setters
public class ApiSettings
{
public required string BaseUrl { get; init; }
public int Timeout { get; init; } = 30;
}Complex Logic in Options Classes
Problem: Adding business logic to configuration POCOs.
// BAD: Options class with logic
public class RetrySettings
{
public int MaxAttempts { get; init; }
public int BaseDelayMs { get; init; }
// Don't put business logic here
public TimeSpan GetDelay(int attempt) =>
TimeSpan.FromMilliseconds(BaseDelayMs * Math.Pow(2, attempt));
}
// GOOD: Keep options as pure data; logic belongs in services
public class RetrySettings
{
public int MaxAttempts { get; init; } = 3;
public int BaseDelayMs { get; init; } = 100;
}
public class RetryPolicy(IOptions<RetrySettings> options)
{
public TimeSpan GetDelay(int attempt) =>
TimeSpan.FromMilliseconds(options.Value.BaseDelayMs * Math.Pow(2, attempt));
}---
Logging Anti-Patterns
String Interpolation in Log Messages
Problem: String interpolation defeats structured logging and always allocates.
// BAD: String interpolation
logger.LogInformation($"Processing order {order.Id} for customer {order.CustomerId}");
// GOOD: Message templates
logger.LogInformation("Processing order {OrderId} for customer {CustomerId}",
order.Id, order.CustomerId);Logging Sensitive Data
Problem: Accidentally logging PII, credentials, or secrets.
// BAD: Logging sensitive data
logger.LogDebug("User login: {Email} with password {Password}", email, password);
logger.LogInformation("Processing payment for card {CardNumber}", cardNumber);
// GOOD: Redact or omit sensitive data
logger.LogDebug("User login attempt for {Email}", email);
logger.LogInformation("Processing payment for card ending in {CardLast4}",
cardNumber[^4..]);Incorrect Log Levels
Problem: Using wrong log levels makes filtering difficult.
// BAD: Using Information for errors
logger.LogInformation("Failed to connect to database: {Error}", ex.Message);
// BAD: Using Error for normal operations
logger.LogError("Request completed successfully");
// BAD: Using Debug for critical failures
logger.LogDebug("Payment processing failed: {Error}", ex.Message);
// GOOD: Match level to severity
logger.LogDebug("Entering method {MethodName}", nameof(ProcessAsync));
logger.LogInformation("Order {OrderId} processed successfully", orderId);
logger.LogWarning("Retry attempt {Attempt} of {MaxAttempts}", attempt, max);
logger.LogError(ex, "Payment {PaymentId} failed", paymentId);
logger.LogCritical("Database connection pool exhausted");Missing Exception in Log
Problem: Logging exception message but not the exception itself.
// BAD: Loses stack trace and exception type
catch (Exception ex)
{
logger.LogError("Operation failed: {Message}", ex.Message);
}
// GOOD: Pass exception as first parameter
catch (Exception ex)
{
logger.LogError(ex, "Operation failed for order {OrderId}", orderId);
}Logging Inside Tight Loops
Problem: Excessive logging in hot paths kills performance.
// BAD: Logging every iteration
foreach (var item in items) // Could be millions
{
logger.LogDebug("Processing item {ItemId}", item.Id);
Process(item);
}
// GOOD: Log summary or use conditional logging
logger.LogInformation("Processing {Count} items", items.Count);
foreach (var item in items)
{
Process(item);
}
logger.LogInformation("Completed processing {Count} items", items.Count);---
HttpClient Anti-Patterns
Creating HttpClient Directly
Problem: Not using IHttpClientFactory leads to socket exhaustion.
// BAD: Creates new HttpClient per request
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient(); // Socket exhaustion risk
return await client.GetStringAsync(url);
}
// BAD: Singleton HttpClient ignores DNS changes
private static readonly HttpClient _client = new();
// GOOD: Use IHttpClientFactory
public class ApiClient(HttpClient httpClient)
{
public Task<string> GetDataAsync(string path) =>
httpClient.GetStringAsync(path);
}Not Disposing HttpResponseMessage
Problem: Leaking connections by not disposing responses.
// BAD: Response not disposed
public async Task<string?> GetValueAsync(string url)
{
var response = await httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null; // Response not disposed!
return await response.Content.ReadAsStringAsync();
}
// GOOD: Always dispose response
public async Task<string?> GetValueAsync(string url)
{
using var response = await httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync();
}Ignoring Cancellation Tokens
Problem: Not passing cancellation tokens to async operations.
// BAD: No cancellation support
public async Task<Data> GetDataAsync()
{
var response = await httpClient.GetAsync("api/data");
return await response.Content.ReadFromJsonAsync<Data>();
}
// GOOD: Support cancellation throughout
public async Task<Data?> GetDataAsync(CancellationToken cancellationToken = default)
{
using var response = await httpClient.GetAsync("api/data", cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Data>(cancellationToken);
}---
Hosted Service Anti-Patterns
Blocking in ExecuteAsync
Problem: Blocking the hosted service startup.
// BAD: Blocks application startup
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Thread.Sleep(1000); // Blocks thread!
await ProcessAsync();
}
}
// GOOD: Use async delays
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}Not Handling Exceptions
Problem: Unhandled exceptions crash the hosted service silently.
// BAD: Unhandled exception stops the service
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken); // Exception kills the loop
}
}
// GOOD: Handle exceptions and continue
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Expected during shutdown
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Error in background processing");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}Injecting Scoped Services Directly
Problem: Scoped services injected into singleton hosted services.
// BAD: Scoped DbContext in singleton BackgroundService
public class DataSyncService(AppDbContext dbContext) : BackgroundService
{
// dbContext is now a captive dependency with wrong lifetime
}
// GOOD: Create scope for each unit of work
public class DataSyncService(
IServiceScopeFactory scopeFactory,
ILogger<DataSyncService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await SyncDataAsync(dbContext, stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}---
Generic Host Anti-Patterns
Long-Running StartAsync
Problem: Blocking application startup in IHostedService.StartAsync.
// BAD: Blocks entire application startup
public class WarmupService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
await LoadEntireCacheAsync(); // Takes 30 seconds!
}
}
// GOOD: Start work in background, return quickly
public class WarmupService(ILogger<WarmupService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Starting cache warmup");
await LoadEntireCacheAsync(stoppingToken);
logger.LogInformation("Cache warmup complete");
}
}Ignoring Application Lifetime Events
Problem: Not cleaning up resources during shutdown.
// BAD: No graceful shutdown handling
public class WorkerService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ProcessBatchAsync(); // May be interrupted mid-batch
}
}
}
// GOOD: Handle shutdown gracefully
public class WorkerService(
IHostApplicationLifetime lifetime,
ILogger<WorkerService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
lifetime.ApplicationStopping.Register(() =>
logger.LogInformation("Shutdown requested, finishing current batch..."));
while (!stoppingToken.IsCancellationRequested)
{
await ProcessBatchAsync(stoppingToken);
}
logger.LogInformation("Graceful shutdown complete");
}
}Microsoft.Extensions Patterns
Dependency Injection Patterns
Constructor Injection with Primary Constructors
Primary constructors (C# 12+) provide a concise way to declare dependencies:
public class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger,
IOptions<OrderSettings> options)
{
public async Task<Order> GetOrderAsync(int id)
{
logger.LogDebug("Fetching order {OrderId}", id);
return await repository.GetByIdAsync(id);
}
}Service Registration Extensions
Encapsulate related registrations in extension methods:
public static class OrderingServiceExtensions
{
public static IServiceCollection AddOrdering(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<OrderSettings>(configuration.GetSection("Ordering"));
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
return services;
}
}Usage in Program.cs:
builder.Services.AddOrdering(builder.Configuration);Keyed Services (C# 12+ / .NET 8+)
Register and resolve multiple implementations by key:
// Registration
services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
// Injection via primary constructor
public class CheckoutService(
[FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor,
[FromKeyedServices("paypal")] IPaymentProcessor paypalProcessor)
{
public Task ProcessAsync(string provider, decimal amount) =>
provider switch
{
"stripe" => stripeProcessor.ChargeAsync(amount),
"paypal" => paypalProcessor.ChargeAsync(amount),
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}Factory Pattern with DI
When you need runtime parameters to create services:
public interface IConnectionFactory
{
IConnection Create(string connectionString);
}
public class ConnectionFactory(ILogger<ConnectionFactory> logger) : IConnectionFactory
{
public IConnection Create(string connectionString)
{
logger.LogDebug("Creating connection to {ConnectionString}", connectionString);
return new Connection(connectionString);
}
}Decorator Pattern
Wrap existing services with additional behavior:
public class CachingOrderRepository(
IOrderRepository inner,
IMemoryCache cache,
ILogger<CachingOrderRepository> logger) : IOrderRepository
{
public async Task<Order?> GetByIdAsync(int id)
{
var cacheKey = $"order:{id}";
if (cache.TryGetValue(cacheKey, out Order? cached))
{
logger.LogDebug("Cache hit for order {OrderId}", id);
return cached;
}
var order = await inner.GetByIdAsync(id);
if (order is not null)
{
cache.Set(cacheKey, order, TimeSpan.FromMinutes(5));
}
return order;
}
}Registration with Scrutor or manual:
services.AddScoped<IOrderRepository, OrderRepository>();
services.Decorate<IOrderRepository, CachingOrderRepository>();---
Configuration Patterns
Strongly Typed Configuration
Define a POCO for your settings:
public class EmailSettings
{
public const string SectionName = "Email";
public required string SmtpHost { get; init; }
public int SmtpPort { get; init; } = 587;
public required string FromAddress { get; init; }
public bool UseSsl { get; init; } = true;
}Register and bind:
services.Configure<EmailSettings>(configuration.GetSection(EmailSettings.SectionName));Configuration Validation at Startup
Use ValidateDataAnnotations or ValidateOnStart to fail fast:
public class DatabaseSettings
{
[Required]
public required string ConnectionString { get; init; }
[Range(1, 100)]
public int MaxPoolSize { get; init; } = 10;
}
// Registration with validation
services.AddOptions<DatabaseSettings>()
.BindConfiguration("Database")
.ValidateDataAnnotations()
.ValidateOnStart();Custom Validation Logic
services.AddOptions<ApiSettings>()
.BindConfiguration("Api")
.Validate(settings =>
{
if (string.IsNullOrEmpty(settings.BaseUrl))
return false;
return Uri.TryCreate(settings.BaseUrl, UriKind.Absolute, out _);
}, "BaseUrl must be a valid absolute URI")
.ValidateOnStart();Environment-Specific Configuration
Standard configuration layering:
var builder = Host.CreateApplicationBuilder(args);
// Already loaded by CreateApplicationBuilder:
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. Environment variables
// 4. Command-line args
// Add additional sources if needed
builder.Configuration.AddJsonFile("secrets.json", optional: true);Configuration Sections Binding
Bind nested sections:
public class AppSettings
{
public required DatabaseSettings Database { get; init; }
public required EmailSettings Email { get; init; }
public required CacheSettings Cache { get; init; }
}
// Bind entire hierarchy
var settings = configuration.Get<AppSettings>();---
Options Pattern
IOptions vs IOptionsSnapshot vs IOptionsMonitor
| Interface | Lifetime | Reloads | Use Case |
|---|---|---|---|
IOptions<T> | Singleton | No | Static configuration |
IOptionsSnapshot<T> | Scoped | Per request | Web apps with reloadable config |
IOptionsMonitor<T> | Singleton | Yes (notifications) | Long-running services |
Using IOptionsMonitor for Live Updates
public class FeatureFlagService(IOptionsMonitor<FeatureFlags> optionsMonitor)
{
public bool IsEnabled(string featureName) =>
optionsMonitor.CurrentValue.EnabledFeatures.Contains(featureName);
public IDisposable OnChange(Action<FeatureFlags> listener) =>
optionsMonitor.OnChange(listener);
}Named Options
Configure multiple instances of the same type:
services.Configure<StorageOptions>("blob", configuration.GetSection("Storage:Blob"));
services.Configure<StorageOptions>("table", configuration.GetSection("Storage:Table"));
// Injection
public class StorageService(IOptionsSnapshot<StorageOptions> options)
{
public string GetBlobConnectionString() =>
options.Get("blob").ConnectionString;
public string GetTableConnectionString() =>
options.Get("table").ConnectionString;
}Post-Configure
Apply transformations after binding:
services.PostConfigure<ApiSettings>(settings =>
{
settings.BaseUrl = settings.BaseUrl.TrimEnd('/');
});---
Logging Patterns
Structured Logging
Use message templates with named placeholders:
public class PaymentService(ILogger<PaymentService> logger)
{
public async Task ProcessPaymentAsync(Payment payment)
{
logger.LogInformation(
"Processing payment {PaymentId} for {Amount:C} from {CustomerId}",
payment.Id, payment.Amount, payment.CustomerId);
try
{
await ProcessAsync(payment);
logger.LogInformation("Payment {PaymentId} processed successfully", payment.Id);
}
catch (PaymentException ex)
{
logger.LogError(ex,
"Payment {PaymentId} failed: {ErrorCode}",
payment.Id, ex.ErrorCode);
throw;
}
}
}High-Performance Logging with Source Generators
Define log messages at compile time:
public static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Processing order {OrderId} with {ItemCount} items")]
public static partial void ProcessingOrder(ILogger logger, int orderId, int itemCount);
[LoggerMessage(Level = LogLevel.Error, Message = "Order {OrderId} processing failed")]
public static partial void OrderProcessingFailed(ILogger logger, int orderId, Exception exception);
}
// Usage
public class OrderProcessor(ILogger<OrderProcessor> logger)
{
public void Process(Order order)
{
Log.ProcessingOrder(logger, order.Id, order.Items.Count);
}
}Logging Scopes
Add contextual data to all logs within a scope:
public class RequestProcessor(ILogger<RequestProcessor> logger)
{
public async Task ProcessAsync(Request request)
{
using (logger.BeginScope(new Dictionary<string, object>
{
["RequestId"] = request.Id,
["UserId"] = request.UserId,
["CorrelationId"] = request.CorrelationId
}))
{
logger.LogInformation("Starting request processing");
await DoWorkAsync(request);
logger.LogInformation("Request processing completed");
}
}
}Category-Based Filtering
Configure log levels per category in appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"MyApp.DataAccess": "Debug",
"MyApp.Services": "Information"
}
}
}---
HttpClientFactory Patterns
Typed Clients
public class GitHubClient(HttpClient httpClient, ILogger<GitHubClient> logger)
{
public async Task<User?> GetUserAsync(string username)
{
logger.LogDebug("Fetching GitHub user {Username}", username);
return await httpClient.GetFromJsonAsync<User>($"users/{username}");
}
}
// Registration
services.AddHttpClient<GitHubClient>(client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
});Resilience with Polly
services.AddHttpClient<ExternalApiClient>()
.AddStandardResilienceHandler();
// Or custom policies
services.AddHttpClient<ExternalApiClient>()
.AddResilienceHandler("custom", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential
});
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = TimeSpan.FromSeconds(30),
FailureRatio = 0.5,
MinimumThroughput = 10
});
});Named Clients
services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
});
// Usage via factory
public class MultiApiService(IHttpClientFactory clientFactory)
{
public async Task<string> GetDataAsync()
{
var client = clientFactory.CreateClient("github");
return await client.GetStringAsync("zen");
}
}---
Hosted Service Patterns
Background Worker
public class QueueProcessor(
IServiceScopeFactory scopeFactory,
ILogger<QueueProcessor> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Queue processor starting");
while (!stoppingToken.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var queue = scope.ServiceProvider.GetRequiredService<IMessageQueue>();
if (await queue.TryDequeueAsync(stoppingToken) is { } message)
{
await ProcessMessageAsync(message, scope.ServiceProvider, stoppingToken);
}
else
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
}
}Timed Background Service
public class HealthCheckService(
ILogger<HealthCheckService> logger,
IOptions<HealthCheckOptions> options) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(options.Value.Interval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await PerformHealthCheckAsync(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Health check failed");
}
}
}
}---
Generic Host Patterns
Minimal Console App
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<WorkerService>();
builder.Services.AddOrdering(builder.Configuration);
var host = builder.Build();
await host.RunAsync();Graceful Shutdown
public class GracefulWorker(
IHostApplicationLifetime lifetime,
ILogger<GracefulWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
lifetime.ApplicationStarted.Register(() =>
logger.LogInformation("Application started"));
lifetime.ApplicationStopping.Register(() =>
logger.LogInformation("Application stopping, draining work..."));
// Work loop
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
}
}
}