원클릭으로
dotnet-di
.NET dependency injection patterns and practices
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
.NET dependency injection patterns and practices
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Audit a project against a canon's rules and checklist. Read-only — produces prioritized report without fixing. Works with any canon (nextjs, sql, typescript, etc.).
Lens home base - status, help, and setup
Plan and build a new feature with quality gates.
Simple changes done right. Make the change, clean up after yourself, report what happened.
Review against canons + quality gate, fix findings, verify. Claude-native — no external models.
Plan and improve existing code with quality gates.
| name | dotnet-di |
| description | .NET dependency injection patterns and practices |
Mark Seemann's core belief: Dependency injection is a set of principles, not a framework. Constructor injection is the default. The Composition Root wires everything. Service Locator is an anti-pattern. Get these right, and your code becomes testable, maintainable, and honest about its dependencies.
"Dependency Injection is nothing more than a collection of design principles and patterns that enable loose coupling."
DI is not about containers. It's about writing code that declares what it needs and lets someone else provide it.
Not this:
public class OrderService
{
private IOrderRepository _repo;
public void SetRepository(IOrderRepository repo) { _repo = repo; }
public void PlaceOrder(Order order) { _repo.Save(order); } // Might be null!
}
This:
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void PlaceOrder(Order order)
{
_repo.Save(order); // Always valid
_logger.LogInformation("Order {Id} placed", order.Id);
}
}
Why: Dependencies are immutable (readonly), object is never in an invalid state, dependencies are visible in the signature, guard clauses catch nulls at composition time.
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
| Lifetime | Created | Use for |
|---|---|---|
| Singleton | Once per app | Caches, config, HttpClientFactory |
| Scoped | Once per request | DbContext, Unit of Work, current user |
| Transient | Every injection | Lightweight, stateless services |
Decision rule: Shared state across requests? Singleton. State for one request? Scoped. Stateless? Transient. When in doubt, Scoped.
A singleton depending on a scoped service captures it forever. The scoped instance never disposes.
Not this:
public class ProductCache // Singleton
{
private readonly AppDbContext _db; // Scoped -- CAPTURED!
public ProductCache(AppDbContext db) { _db = db; }
}
This:
public class ProductCache // Singleton
{
private readonly IServiceScopeFactory _scopeFactory;
public ProductCache(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; }
public async Task<Product?> GetFreshAsync(int id)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await db.Products.FindAsync(id);
}
}
Detect it: ValidateScopes = true in development throws on captive dependencies.
The rule: A service can only depend on equal or longer lifetimes: Singleton -> Singleton. Scoped -> Singleton, Scoped. Transient -> any.
All registration in Program.cs. Nowhere else resolves from the container.
Not this (Service Locator):
public class OrderService
{
public void PlaceOrder(Order order)
{
var repo = ServiceLocator.Get<IOrderRepository>(); // Hidden dependency!
repo.Save(order);
}
}
This:
// Program.cs -- the ONE place that knows about concrete types
builder.Services.AddOrderingServices();
public static class OrderingServiceExtensions
{
public static IServiceCollection AddOrderingServices(this IServiceCollection services)
{
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IOrderService, OrderService>();
return services;
}
}
Service Locator hides dependencies, kills compile-time safety, and makes testing require a full container.
Not this:
public interface IUserService // 8 methods -- GetById, Create, Delete, Verify, Reset...
{
bool ValidateCredentials(string email, string password);
// ... 7 more methods
}
public class LoginHandler
{
private readonly IUserService _users; // Only needs ValidateCredentials!
public LoginHandler(IUserService users) => _users = users;
}
This:
public interface ICredentialValidator
{
bool Validate(string email, string password);
}
public class LoginHandler
{
private readonly ICredentialValidator _validator; // Exactly what it needs
public LoginHandler(ICredentialValidator validator) => _validator = validator;
}
Constructor tells the truth. Tests mock only what's needed. Unrelated changes don't ripple.
For runtime instance creation, inject a factory -- not IServiceProvider.
Func factory:
builder.Services.AddTransient<INotificationSender, EmailNotificationSender>();
builder.Services.AddSingleton<Func<INotificationSender>>(
sp => () => sp.GetRequiredService<INotificationSender>());
Custom factory when creation needs parameters:
public class ReportGeneratorFactory : IReportGeneratorFactory
{
private readonly IServiceProvider _sp;
public ReportGeneratorFactory(IServiceProvider sp) => _sp = sp;
public IReportGenerator Create(ReportFormat format) => format switch
{
ReportFormat.Pdf => _sp.GetRequiredService<PdfReportGenerator>(),
ReportFormat.Excel => _sp.GetRequiredService<ExcelReportGenerator>(),
_ => throw new ArgumentOutOfRangeException(nameof(format))
};
}
Confine IServiceProvider to factory classes. Never inject it into business logic.
Not this:
public class EmailSender
{
public EmailSender(IConfiguration config) // God object, magic strings
{
var host = config["Smtp:Host"];
var port = int.Parse(config["Smtp:Port"]);
}
}
This:
public class SmtpSettings
{
public string Host { get; set; } = "";
public int Port { get; set; } = 587;
}
// Program.cs
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
// Usage
public class EmailSender
{
private readonly SmtpSettings _settings;
public EmailSender(IOptions<SmtpSettings> options) { _settings = options.Value; }
}
Named options for multiple instances: Configure<ApiSettings>("GitHub", section), resolve with IOptionsMonitor<T>.Get("GitHub").
IOptions<T> = singleton, read once. IOptionsSnapshot<T> = scoped, refreshes per request. IOptionsMonitor<T> = singleton, refreshes on change.
Not this:
public INotificationSender Create(string channel) => channel switch
{
"email" => new EmailSender(), // Newing up = untestable
"sms" => new SmsSender(),
_ => throw new ArgumentException(channel)
};
This:
builder.Services.AddKeyedScoped<INotificationSender, EmailSender>("email");
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>("sms");
builder.Services.AddKeyedScoped<INotificationSender, PushSender>("push");
public class OrderNotifier
{
public OrderNotifier(
[FromKeyedServices("email")] INotificationSender email,
[FromKeyedServices("sms")] INotificationSender sms)
{ }
}
Wrap services with cross-cutting concerns without modifying them.
// Core
public class SqlOrderRepository : IOrderRepository { /* DB queries */ }
// Caching decorator
public class CachedOrderRepository : IOrderRepository
{
private readonly IOrderRepository _inner;
private readonly IMemoryCache _cache;
public CachedOrderRepository(IOrderRepository inner, IMemoryCache cache)
{ _inner = inner; _cache = cache; }
public async Task<Order?> GetByIdAsync(int id) =>
await _cache.GetOrCreateAsync($"order:{id}",
_ => _inner.GetByIdAsync(id));
public async Task SaveAsync(Order order)
{ await _inner.SaveAsync(order); _cache.Remove($"order:{order.Id}"); }
}
Registration (manual chaining):
builder.Services.AddScoped<SqlOrderRepository>();
builder.Services.AddScoped<IOrderRepository>(sp =>
new CachedOrderRepository(
sp.GetRequiredService<SqlOrderRepository>(),
sp.GetRequiredService<IMemoryCache>()));
With Scrutor: builder.Services.Decorate<IOrderRepository, CachedOrderRepository>();
Unit tests -- no container needed:
[Fact]
public async Task PlaceOrder_SavesAndNotifies()
{
var repo = new FakeOrderRepository();
var notifier = new FakeNotifier();
var sut = new OrderService(repo, notifier);
await sut.PlaceOrderAsync(new Order { Id = 1 });
Assert.Single(repo.SavedOrders);
Assert.Single(notifier.SentNotifications);
}
Integration tests -- WebApplicationFactory overrides:
public class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public OrderApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.WithWebHostBuilder(b =>
b.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<AppDbContext>>();
services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("Test"));
services.AddScoped<IPaymentGateway, FakePaymentGateway>();
})).CreateClient();
}
[Fact]
public async Task PostOrder_Returns201()
{
var response = await _client.PostAsJsonAsync("/api/orders", new { ProductId = 1 });
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
| Anti-Pattern | What It Is | Fix |
|---|---|---|
| Service Locator | Resolving from container in business logic | Constructor injection |
| Constructor Over-Injection | 5+ parameters = class does too much | Extract a service grouping related deps |
| Ambient Context | HttpContext.Current, DateTime.Now | Inject IHttpContextAccessor, IDateTimeProvider |
| Bastard Injection | Default impl in constructor: repo ?? new SqlRepo() | Remove default, require explicit injection |
| Control Freak | newing dependencies inside the class | Inject via constructor |
SITUATION APPROACH
----------------------------------------------------------------------------------
Default for all dependencies Constructor injection, readonly fields
Runtime instance creation Factory (Func<T> or custom interface)
Same interface, multiple impls Keyed services (.NET 8) or named options
Configuration values IOptions<T> / IOptionsSnapshot<T>
Cross-cutting concerns Decorator wrapping inner service
Singleton needs scoped dependency IServiceScopeFactory, create scope on demand
Which lifetime? Scoped unless proven otherwise
Testing business logic Pass fakes via constructor, no container
Testing HTTP endpoints WebApplicationFactory with service overrides
readonly fields?IServiceProvider in business logic?ValidateScopes = true in Development?IConfiguration injection for settings?Program.cs or extensions?"Dependency Injection is nothing more than a collection of design principles and patterns that enable loose coupling." -- Mark Seemann