Master ASP.NET Core dependency injection: service registration, lifetimes (singleton/scoped/transient), patterns, and best practices.
Dependency Injection (DI) is a first-class citizen in ASP.NET Core. The built-in DI container manages object creation, lifetime, and disposal, enabling loose coupling, easy testing with mocks, and a clean separation of concerns throughout your application.
Master ASP.NET Core's built-in DI container
Dependency Injection (DI) is a design pattern that helps achieve Inversion of Control (IoC) between classes and their dependencies. ASP.NET Core has a built-in DI container that manages the lifetime and resolution of dependencies.
1// Bad: Tightly coupled - hard to test
2public class OrderService
3{
4 private readonly EmailService _emailService;
5 private readonly SqlOrderRepository _repository;
6
7 public OrderService()
8 {
9 // Creates its own dependencies - tight coupling!
10 _emailService = new EmailService();
11 _repository = new SqlOrderRepository();
12 }
13
14 public void PlaceOrder(Order order)
15 {
16 _repository.Save(order);
17 _emailService.SendConfirmation(order);
18 }
19}1// Good: Dependencies injected - loose coupling
2public interface IEmailService
3{
4 Task SendConfirmationAsync(Order order);
5}
6
7public interface IOrderRepository
8{
9 Task SaveAsync(Order order);
10}
11
12public class OrderService
13{
14 private readonly IEmailService _emailService;
15 private readonly IOrderRepository _repository;
16
17 // Dependencies injected via constructor
18 public OrderService(
19 IEmailService emailService,
20 IOrderRepository repository)
21 {
22 _emailService = emailService;
23 _repository = repository;
24 }
25
26 public async Task PlaceOrderAsync(Order order)
27 {
28 await _repository.SaveAsync(order);
29 await _emailService.SendConfirmationAsync(order);
30 }
31}
32
33// Registration in Program.cs
34builder.Services.AddScoped<IEmailService, SmtpEmailService>();
35builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
36builder.Services.AddScoped<OrderService>();ASP.NET Core supports three service lifetimes:
1// Three service lifetimes in ASP.NET Core
2
3// 1. TRANSIENT - New instance every time
4builder.Services.AddTransient<ITransientService, TransientService>();
5// Use for: Lightweight, stateless services
6// Created: Every time they are requested
7
8// 2. SCOPED - One instance per HTTP request
9builder.Services.AddScoped<IScopedService, ScopedService>();
10// Use for: Database contexts, unit of work, per-request services
11// Created: Once per HTTP request, shared within that request
12
13// 3. SINGLETON - One instance for entire application
14builder.Services.AddSingleton<ISingletonService, SingletonService>();
15// Use for: Configuration, logging, caching services
16// Created: Once when first requested, shared across all requestsVisualization:
1Request 1 Request 2
2───────────────────────────────── ─────────────────────────────────
3┌─────────────────────────────┐ ┌─────────────────────────────┐
4│ Transient A (new) │ │ Transient A (new) │
5│ Transient B (new) │ │ Transient B (new) │
6│ │ │ │
7│ Scoped A (shared) ───┼────│ Scoped A (new) │
8│ Scoped A (same) │ │ Scoped A (same) │
9│ │ │ │
10│ Singleton A (shared) ═══╪════│ Singleton A (same) │
11└─────────────────────────────┘ └─────────────────────────────┘
12
13Transient: Different instance every injection
14Scoped: Same instance within request, different across requests
15Singleton: Same instance everywhereDemonstrating lifetimes with unique IDs:
1// Services with unique IDs
2public class OperationService
3{
4 public string Id { get; } = Guid.NewGuid().ToString()[..8];
5}
6
7public class TransientOperation : OperationService { }
8public class ScopedOperation : OperationService { }
9public class SingletonOperation : OperationService { }
10
11// Registration
12builder.Services.AddTransient<TransientOperation>();
13builder.Services.AddScoped<ScopedOperation>();
14builder.Services.AddSingleton<SingletonOperation>();
15
16// Controller to demonstrate
17[ApiController]
18[Route("api/[controller]")]
19public class LifetimeController : ControllerBase
20{
21 private readonly TransientOperation _transient1;
22 private readonly TransientOperation _transient2;
23 private readonly ScopedOperation _scoped1;
24 private readonly ScopedOperation _scoped2;
25 private readonly SingletonOperation _singleton1;
26 private readonly SingletonOperation _singleton2;
27
28 public LifetimeController(
29 TransientOperation t1, TransientOperation t2,
30 ScopedOperation s1, ScopedOperation s2,
31 SingletonOperation sg1, SingletonOperation sg2)
32 {
33 _transient1 = t1; _transient2 = t2;
34 _scoped1 = s1; _scoped2 = s2;
35 _singleton1 = sg1; _singleton2 = sg2;
36 }
37
38 [HttpGet]
39 public IActionResult Get()
40 {
41 return Ok(new
42 {
43 Transient = new { First = _transient1.Id, Second = _transient2.Id },
44 Scoped = new { First = _scoped1.Id, Second = _scoped2.Id },
45 Singleton = new { First = _singleton1.Id, Second = _singleton2.Id }
46 });
47 }
48}
49
50/* Response:
51{
52 "transient": { "first": "abc12345", "second": "def67890" }, // Different!
53 "scoped": { "first": "ghi11111", "second": "ghi11111" }, // Same
54 "singleton": { "first": "jkl22222", "second": "jkl22222" } // Same (always)
55}
56*/The DI container supports multiple registration patterns:
1// Basic registration patterns
2var builder = WebApplication.CreateBuilder(args);
3
4// 1. Interface to implementation
5builder.Services.AddScoped<IUserService, UserService>();
6
7// 2. Concrete type only
8builder.Services.AddScoped<EmailService>();
9
10// 3. Factory registration
11builder.Services.AddScoped<IDbConnection>(sp =>
12{
13 var config = sp.GetRequiredService<IConfiguration>();
14 var connectionString = config.GetConnectionString("Default");
15 return new SqlConnection(connectionString);
16});
17
18// 4. Instance registration (singleton only)
19var config = new AppConfiguration { ApiKey = "abc123" };
20builder.Services.AddSingleton(config);
21
22// 5. TryAdd - Only add if not already registered
23builder.Services.TryAddScoped<IUserService, UserService>();
24
25// 6. Multiple implementations
26builder.Services.AddScoped<INotificationService, EmailNotification>();
27builder.Services.AddScoped<INotificationService, SmsNotification>();
28builder.Services.AddScoped<INotificationService, PushNotification>();
29
30// Get all implementations
31public class NotificationManager
32{
33 private readonly IEnumerable<INotificationService> _services;
34
35 public NotificationManager(IEnumerable<INotificationService> services)
36 {
37 _services = services; // All 3 implementations
38 }
39
40 public async Task NotifyAllAsync(string message)
41 {
42 foreach (var service in _services)
43 await service.SendAsync(message);
44 }
45}
46
47// 7. Keyed services (.NET 8+)
48builder.Services.AddKeyedScoped<IPaymentService, StripePayment>("stripe");
49builder.Services.AddKeyedScoped<IPaymentService, PayPalPayment>("paypal");
50
51public class CheckoutService
52{
53 private readonly IPaymentService _payment;
54
55 public CheckoutService(
56 [FromKeyedServices("stripe")] IPaymentService payment)
57 {
58 _payment = payment;
59 }
60}
61
62// 8. Register open generics
63builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
64
65// Usage
66public class ProductController
67{
68 private readonly IRepository<Product> _products; // Repository<Product>
69 private readonly IRepository<Order> _orders; // Repository<Order>
70}Prefer constructor injection, avoid the service locator anti-pattern, and organize registrations:
1// 1. Prefer constructor injection
2public class OrderService
3{
4 private readonly IOrderRepository _repository;
5 private readonly ILogger<OrderService> _logger;
6
7 // Constructor injection - explicit dependencies
8 public OrderService(
9 IOrderRepository repository,
10 ILogger<OrderService> logger)
11 {
12 _repository = repository ?? throw new ArgumentNullException(nameof(repository));
13 _logger = logger ?? throw new ArgumentNullException(nameof(logger));
14 }
15}
16
17// 2. Avoid Service Locator pattern
18// BAD - Service locator anti-pattern
19public class BadService
20{
21 private readonly IServiceProvider _provider;
22
23 public void DoSomething()
24 {
25 // Hidden dependency - hard to test
26 var repo = _provider.GetService<IOrderRepository>();
27 }
28}
29
30// GOOD - Explicit dependency
31public class GoodService
32{
33 private readonly IOrderRepository _repo;
34
35 public GoodService(IOrderRepository repo) => _repo = repo;
36}
37
38// 3. Use IOptions<T> for configuration
39public class EmailSettings
40{
41 public string SmtpServer { get; set; } = "";
42 public int Port { get; set; }
43 public string FromAddress { get; set; } = "";
44}
45
46builder.Services.Configure<EmailSettings>(
47 builder.Configuration.GetSection("Email"));
48
49public class EmailService
50{
51 private readonly EmailSettings _settings;
52
53 public EmailService(IOptions<EmailSettings> options)
54 {
55 _settings = options.Value;
56 }
57}
58
59// 4. Organize registrations with extension methods
60public static class ServiceCollectionExtensions
61{
62 public static IServiceCollection AddApplicationServices(
63 this IServiceCollection services)
64 {
65 services.AddScoped<IOrderService, OrderService>();
66 services.AddScoped<IUserService, UserService>();
67 services.AddScoped<IEmailService, EmailService>();
68 return services;
69 }
70
71 public static IServiceCollection AddInfrastructure(
72 this IServiceCollection services,
73 IConfiguration config)
74 {
75 services.AddDbContext<AppDbContext>(options =>
76 options.UseNpgsql(config.GetConnectionString("Default")));
77
78 services.AddScoped<IOrderRepository, OrderRepository>();
79 services.AddScoped<IUserRepository, UserRepository>();
80
81 return services;
82 }
83}
84
85// Clean Program.cs
86builder.Services.AddApplicationServices();
87builder.Services.AddInfrastructure(builder.Configuration);Common pitfalls: captive dependencies (scoped into singleton), circular dependencies, and too many constructor parameters (5+ signals a refactoring need).
Your progress is saved in this browser, on this device only.