Claude
Skill
dotnet-microsoft-extensions
Use the Microsoft.Extensions stack correctly across Generic Host, dependency injection, configuration, logging, options, HttpClientFactory, and other shared infrastructure patterns.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-microsoft-extensions-bfa4ebd.zip · 9 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-microsoft-extensions
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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
- Prefer the Generic Host for apps that need configuration, DI, logging, hosted services, or coordinated startup.
- Keep service registration predictable: composition at the edge, concrete implementations hidden behind interfaces only where that abstraction buys flexibility.
- Use options binding for structured configuration and validate configuration at startup when bad settings would fail later at runtime.
- Prefer
IHttpClientFactoryand typed or named clients for outbound HTTP instead of ad-hoc singleton or per-callHttpClientusage. - Use logging categories and config-driven log levels rather than scattered ad-hoc logging behavior.
- 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
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
Files (dotnet-skills)
-
references
-
anti-patterns.md 16.2 KB
# Microsoft.Extensions Anti-Patterns ## Dependency Injection Anti-Patterns ### Service Locator Pattern **Problem**: Resolving services manually instead of using constructor injection. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```json // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp // 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`. ```csharp // 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. ```csharp // 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"); } } ``` -
patterns.md 13.1 KB
# Microsoft.Extensions Patterns ## Dependency Injection Patterns ### Constructor Injection with Primary Constructors Primary constructors (C# 12+) provide a concise way to declare dependencies: ```csharp 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: ```csharp 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`: ```csharp builder.Services.AddOrdering(builder.Configuration); ``` ### Keyed Services (C# 12+ / .NET 8+) Register and resolve multiple implementations by key: ```csharp // 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: ```csharp 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: ```csharp 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: ```csharp services.AddScoped<IOrderRepository, OrderRepository>(); services.Decorate<IOrderRepository, CachingOrderRepository>(); ``` --- ## Configuration Patterns ### Strongly Typed Configuration Define a POCO for your settings: ```csharp 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: ```csharp services.Configure<EmailSettings>(configuration.GetSection(EmailSettings.SectionName)); ``` ### Configuration Validation at Startup Use `ValidateDataAnnotations` or `ValidateOnStart` to fail fast: ```csharp 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 ```csharp 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: ```csharp 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: ```csharp 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 ```csharp 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: ```csharp 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: ```csharp services.PostConfigure<ApiSettings>(settings => { settings.BaseUrl = settings.BaseUrl.TrimEnd('/'); }); ``` --- ## Logging Patterns ### Structured Logging Use message templates with named placeholders: ```csharp 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: ```csharp 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: ```csharp 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`: ```json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "MyApp.DataAccess": "Debug", "MyApp.Services": "Information" } } } ``` --- ## HttpClientFactory Patterns ### Typed Clients ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService<WorkerService>(); builder.Services.AddOrdering(builder.Configuration); var host = builder.Build(); await host.RunAsync(); ``` ### Graceful Shutdown ```csharp 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); } } } ```
-
-
SKILL.md 2 KB
--- name: dotnet-microsoft-extensions version: "1.0.0" category: "Core" description: "Use the Microsoft.Extensions stack correctly across Generic Host, dependency injection, configuration, logging, options, HttpClientFactory, and other shared infrastructure patterns." compatibility: "Relevant to console apps, workers, ASP.NET Core apps, functions, and reusable libraries." --- # 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. 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 ## References - [patterns.md](references/patterns.md) - DI patterns, Configuration patterns, Options pattern, Logging patterns, HttpClientFactory patterns, Hosted Service patterns - [anti-patterns.md](references/anti-patterns.md) - Common mistakes with DI, configuration, options, logging, HttpClient, and hosted services
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.