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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-microsoft-extensions-bfa4ebd.zip · 9 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

  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 - 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.

No comments yet.

Reviews (0)

No reviews yet.

Related