SKILL.md
$27
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IEmailComposer, MjmlEmailComposer>();
builder.Services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<IInvoiceGenerator, InvoiceGenerator>();
// ... 150 more lines ...
Problems:
- Hard to find related registrations
- No clear boundaries between subsystems
- Can't reuse configuration in tests
- Merge conflicts in team settings
- No encapsulation of internal dependencies
---
## The Solution: Extension Method Composition
Group related registrations into extension methods:
// GOOD: Clean, composable Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddUserServices()
.AddOrderServices()
.AddEmailServices()
.AddPaymentServices()
.AddValidators();
var app = builder.Build();
Each `Add*` method encapsulates a cohesive set of registrations.
## Extension Method Pattern
### Basic Structure
namespace MyApp.Users;
public static class UserServiceCollectionExtensions
{
public static IServiceCollection AddUserServices(this IServiceCollection services)
{
// Repositories
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserReadStore, UserReadStore>();
services.AddScoped<IUserWriteStore, UserWriteStore>();
// Services
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserValidationService, UserValidationService>();
// Return for chaining
return services;
}
}
### With Configuration
namespace MyApp.Email;
public static class EmailServiceCollectionExtensions
{
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
string configSectionName = "EmailSettings")
{
// Bind configuration
services.AddOptions<EmailOptions>()
.BindConfiguration(configSectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// Register services
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
services.AddScoped<IUserEmailComposer, UserEmailComposer>();
services.AddScoped<IOrderEmailComposer, OrderEmailComposer>();
// SMTP client depends on environment
services.AddScoped<IEmailSender, SmtpEmailSender>();
return services;
}
}
### With Dependencies on Other Extensions
namespace MyApp.Orders;
public static class OrderServiceCollectionExtensions
{
public static IServiceCollection AddOrderServices(this IServiceCollection services)
{
// This subsystem depends on email services
// Caller is responsible for calling AddEmailServices() first
// Or we can call it here if it's idempotent
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderEmailNotifier, OrderEmailNotifier>();
return services;
}
}
## File Organization
Place extension methods near the services they register:
src/
MyApp.Api/
Program.cs # Composes all Add* methods
MyApp.Users/
Services/
UserService.cs
IUserService.cs
Repositories/
UserRepository.cs
UserServiceCollectionExtensions.cs # AddUserServices()
MyApp.Orders/
Services/
OrderService.cs
OrderServiceCollectionExtensions.cs # AddOrderServices()
MyApp.Email/
Composers/
UserEmailComposer.cs
EmailServiceCollectionExtensions.cs # AddEmailServices()
**Convention**: `{Feature}ServiceCollectionExtensions.cs` next to the feature's services.
## Naming Conventions
Pattern
Use For
`Add{Feature}Services()`
General feature registration
`Add{Feature}()`
Short form when unambiguous
`Configure{Feature}()`
When primarily setting options
`Use{Feature}()`
Middleware (on IApplicationBuilder)
// Feature services
services.AddUserServices();
services.AddEmailServices();
services.AddPaymentServices();
// Third-party integrations
services.AddStripePayments();
services.AddSendGridEmail();
// Configuration-heavy
services.ConfigureAuthentication();
services.ConfigureAuthorization();
## Testing Benefits
The main advantage: **reuse production configuration in tests**.
### WebApplicationFactory
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Production services already registered via Add* methods
// Only override what's different for testing
// Replace email sender with test double
services.RemoveAll<IEmailSender>();
services.AddSingleton<IEmailSender, TestEmailSender>();
// Replace external payment processor
services.RemoveAll<IPaymentProcessor>();
services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
});
});
}
[Fact]
public async Task CreateOrder_SendsConfirmationEmail()
{
var client = _factory.CreateClient();
var emailSender = _factory.Services.GetRequiredService<IEmailSender>() as TestEmailSender;
await client.PostAsJsonAsync("/api/orders", new CreateOrderRequest(...));
Assert.Single(emailSender!.SentEmails);
}
}
### Akka.Hosting.TestKit
public class OrderActorSpecs : Akka.Hosting.TestKit.TestKit
{
protected override void ConfigureAkka(AkkaConfigurationBuilder builder, IServiceProvider provider)
{
// Reuse production Akka configuration
builder.AddOrderActors();
}
protected override void ConfigureServices(IServiceCollection services)
{
// Reuse production service configuration
services.AddOrderServices();
// Override only external dependencies
services.RemoveAll<IPaymentProcessor>();
services.AddSingleton<IPaymentProcessor, FakePaymentProcessor>();
}
[Fact]
public async Task OrderActor_ProcessesPayment()
{
var orderActor = ActorRegistry.Get<OrderActor>();
orderActor.Tell(new ProcessOrder(orderId));
ExpectMsg<OrderProcessed>();
}
}
### Standalone Unit Tests
public class UserServiceTests
{
private readonly ServiceProvider _provider;
public UserServiceTests()
{
var services = new ServiceCollection();
// Reuse production registrations
services.AddUserServices();
// Add test infrastructure
services.AddSingleton<IUserRepository, InMemoryUserRepository>();
_provider = services.BuildServiceProvider();
}
[Fact]
public async Task CreateUser_ValidData_Succeeds()
{
var service = _provider.GetRequiredService<IUserService>();
var result = await service.CreateUserAsync(new CreateUserRequest(...));
Assert.True(result.IsSuccess);
}
}
## Layered Extensions
For larger applications, compose extensions hierarchically:
// Top-level: Everything the app needs
public static class AppServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
return services
.AddDomainServices()
.AddInfrastructureServices()
.AddApiServices();
}
}
// Domain layer
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
return services
.AddUserServices()
.AddOrderServices()
.AddProductServices();
}
}
// Infrastructure layer
public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
{
return services
.AddEmailServices()
.AddPaymentServices()
.AddStorageServices();
}
}
## Akka.Hosting Integration
The same pattern works for Akka.NET actor configuration:
public static class OrderActorExtensions
{
public static AkkaConfigurationBuilder AddOrderActors(
this AkkaConfigurationBuilder builder)
{
return builder
.WithActors((system, registry, resolver) =>
{
var orderProps = resolver.Props<OrderActor>();
var orderRef = system.ActorOf(orderProps, "orders");
registry.Register<OrderActor>(orderRef);
})
.WithShardRegion<OrderShardActor>(
typeName: "order-shard",
(system, registry, resolver) =>
entityId => resolver.Props<OrderShardActor>(entityId),
new OrderMessageExtractor(),
ShardOptions.Create());
}
}
// Usage in Program.cs
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
builder
.AddOrderActors()
.AddInventoryActors()
.AddNotificationActors();
});
See `akka/hosting-actor-patterns` skill for complete Akka.Hosting patterns.
## Common Patterns
### Conditional Registration
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
IHostEnvironment environment)
{
services.AddSingleton<IEmailComposer, MjmlEmailComposer>();
if (environment.IsDevelopment())
{
// Use Mailpit in development
services.AddSingleton<IEmailSender, MailpitEmailSender>();
}
else
{
// Use real SMTP in production
services.AddSingleton<IEmailSender, SmtpEmailSender>();
}
return services;
}
### Factory-Based Registration
public static IServiceCollection AddPaymentServices(
this IServiceCollection services,
string configSection = "Stripe")
{
services.AddOptions<StripeOptions>()
.BindConfiguration(configSection)
.ValidateOnStart();
// Factory for complex initialization
services.AddSingleton<IPaymentProcessor>(sp =>
{
var options = sp.GetRequiredService<IOptions<StripeOptions>>().Value;
var logger = sp.GetRequiredService<ILogger<StripePaymentProcessor>>();
return new StripePaymentProcessor(options.ApiKey, options.WebhookSecret, logger);
});
return services;
}
### Keyed Services (.NET 8+)
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
// Register multiple implementations with keys
services.AddKeyedSingleton<INotificationSender, EmailNotificationSender>("email");
services.AddKeyedSingleton<INotificationSender, SmsNotificationSender>("sms");
services.AddKeyedSingleton<INotificationSender, PushNotificationSender>("push");
// Resolver that picks the right one
services.AddScoped<INotificationDispatcher, NotificationDispatcher>();
return services;
}
## Anti-Patterns
### Don't: Register Everything in Program.cs
// BAD: Massive Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// ... 200 more lines ...
### Don't: Create Overly Generic Extensions
// BAD: Too vague, doesn't communicate what's registered
public static IServiceCollection AddServices(this IServiceCollection services)
{
// Registers 50 random things
}
### Don't: Hide Important Configuration
// BAD: Buried important settings
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("hardcoded-connection-string")); // Hidden!
}
// GOOD: Accept configuration explicitly
public static IServiceCollection AddDatabase(
this IServiceCollection services,
string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
## Best Practices Summary
Practice
Benefit
Group related services into `Add*` methods
Clean Program.cs, clear boundaries
Place extensions near the services they register
Easy to find and maintain
Return `IServiceCollection` for chaining
Fluent API
Accept configuration parameters
Flexibility
Use consistent naming (`Add{Feature}Services`)
Discoverability
Test by reusing production extensions
Confidence, less duplication
## Lifetime Management
Choose the right lifetime based on state:
Lifetime
Use When
Examples
**Singleton**
Stateless, thread-safe, expensive to create
Configuration, HttpClient factories, caches
**Scoped**
Stateful per-request, database contexts
DbContext, repositories, user context
**Transient**
Lightweight, stateful, cheap to create
Validators, short-lived helpers
### Rules of Thumb
// SINGLETON: Stateless services, shared safely
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
// SCOPED: Database access, per-request state
services.AddScoped<IUserRepository, UserRepository>(); // DbContext dependency
services.AddScoped<IOrderService, OrderService>(); // Uses scoped repos
// TRANSIENT: Cheap, short-lived
services.AddTransient<CreateUserRequestValidator>();
### Scope Requirements
**Scoped services require a scope to exist.** In ASP.NET Core, each HTTP request creates a scope automatically. But in other contexts (background services, actors), you must create scopes manually.
// ASP.NET Controller - scope exists automatically
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService; // Scoped - works!
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
}
// Background Service - no automatic scope!
public class OrderProcessingService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public OrderProcessingService(IServiceProvider serviceProvider)
{
// Inject IServiceProvider, NOT scoped services directly
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// Create scope manually for each unit of work
using var scope = _serviceProvider.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
await orderService.ProcessPendingOrdersAsync(ct);
await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
}
}
## Akka.NET Actor Scope Management
**Actors don't have automatic DI scopes.** If you need scoped services inside an actor, inject `IServiceProvider` and create scopes manually.
### Pattern: Scope Per Message
public sealed class AccountProvisionActor : ReceiveActor
{
private readonly IServiceProvider _serviceProvider;
private readonly IActorRef _mailingActor;
public AccountProvisionActor(
IServiceProvider serviceProvider,
IRequiredActor<MailingActor> mailingActor)
{
_serviceProvider = serviceProvider;
_mailingActor = mailingActor.ActorRef;
ReceiveAsync<ProvisionAccount>(HandleProvisionAccount);
}
private async Task HandleProvisionAccount(ProvisionAccount msg)
{
// Create scope for this message processing
using var scope = _serviceProvider.CreateScope();
// Resolve scoped services
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var emailComposer = scope.ServiceProvider.GetRequiredService<IPaymentEmailComposer>();
// Do work with scoped services
var user = await userManager.FindByIdAsync(msg.UserId);
var order = await orderRepository.CreateAsync(msg.Order);
// DbContext commits when scope disposes
}
}
### Why This Pattern Works
- **Each message gets fresh DbContext** - No stale entity tracking
- **Proper disposal** - Connections released after each message
- **Isolation** - One message's errors don't affect others
- **Testable** - Can inject mock IServiceProvider
### Singleton Services in Actors
For stateless services, inject directly (no scope needed):
public sealed class NotificationActor : ReceiveActor
{
private readonly IEmailLinkGenerator _linkGenerator; // Singleton - OK!
private readonly IActorRef _mailingActor;
public NotificationActor(
IEmailLinkGenerator linkGenerator, // Direct injection
IRequiredActor<MailingActor> mailingActor)
{
_linkGenerator = linkGenerator;
_mailingActor = mailingActor.ActorRef;
Receive<SendWelcomeEmail>(Handle);
}
}
### Akka.DependencyInjection Reference
Akka.NET's DI integration is documented at:
- **Akka.DependencyInjection**: [https://getakka.net/articles/actors/dependency-injection.html](https://getakka.net/articles/actors/dependency-injection.html)
- **Akka.Hosting**: [https://github.com/akkadotnet/Akka.Hosting](https://github.com/akkadotnet/Akka.Hosting)
## Common Mistakes
### Injecting Scoped into Singleton
// BAD: Singleton captures scoped service - stale DbContext!
public class CacheService // Registered as Singleton
{
private readonly IUserRepository _repo; // Scoped!
public CacheService(IUserRepository repo) // Captured at startup!
{
_repo = repo; // This DbContext lives forever - BAD
}
}
// GOOD: Inject factory or IServiceProvider
public class CacheService
{
private readonly IServiceProvider _serviceProvider;
public CacheService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task<User> GetUserAsync(string id)
{
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
return await repo.GetByIdAsync(id);
}
}
### No Scope in Background Work
// BAD: No scope for scoped services
public class BadBackgroundService : BackgroundService
{
private readonly IOrderService _orderService; // Scoped!
public BadBackgroundService(IOrderService orderService)
{
_orderService = orderService; // Will throw or behave unexpectedly
}
}
// GOOD: Create scope for each unit of work
public class GoodBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public GoodBackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
// ...
}
}