Registering Services
π― What You'll Learn
- Different service registration patterns
- Generic type registration
- Open generic registration
- Conditional registration
- Extension methods for organization
- Assembly scanning and auto-registration
- Decorators and forwarding
- Best practices for InvenTrack
Basic Registration Patterns
1. Interface to Implementation
builder.Services.AddScoped<IProductService, ProductService>();
Request IProductService, get ProductService.
2. Concrete Type Only
builder.Services.AddScoped<ProductService>();
Request ProductService directly (no abstraction).
3. Factory Function
builder.Services.AddScoped<IProductService>(serviceProvider =>
{
var repository = serviceProvider.GetRequiredService<IProductRepository>();
var logger = serviceProvider.GetRequiredService<ILogger<ProductService>>();
var config = serviceProvider.GetRequiredService<IConfiguration>();
var maxRetries = config.GetValue<int>("ProductService:MaxRetries");
return new ProductService(repository, logger, maxRetries);
});
4. Instance Registration
var cacheService = new RedisCacheService("localhost:6379");
builder.Services.AddSingleton<ICacheService>(cacheService);
Always registered as Singleton (instance already exists).
Generic Type Registration
Closed Generic Types
// Register IRepository<Product>
builder.Services.AddScoped<IRepository<Product>, Repository<Product>>();
// Register IRepository<Customer>
builder.Services.AddScoped<IRepository<Customer>, Repository<Customer>>();
Open Generic Types
Register a generic service for any type parameter:
// Generic repository interface
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
}
// Generic repository implementation
public class Repository<T> : IRepository<T> where T : class
{
private readonly InvenTrackDbContext _context;
public Repository(InvenTrackDbContext context) => _context = context;
public async Task<T?> GetByIdAsync(int id) =>
await _context.Set<T>().FindAsync(id);
}
// Register for ANY type T
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// Now you can inject IRepository<Product>, IRepository<Customer>, etc.
One registration works for all types! No need to register
IRepository<Product>, IRepository<Customer>, etc.
separately.
Conditional Registration
Environment-Based Registration
if (builder.Environment.IsDevelopment())
{
// Development: Log emails to console
builder.Services.AddScoped<IEmailService, ConsoleEmailService>();
}
else
{
// Production: Send real emails
builder.Services.AddScoped<IEmailService, SendGridEmailService>();
}
Configuration-Based Registration
var useRedis = builder.Configuration.GetValue<bool>("UseRedisCache");
if (useRedis)
{
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
}
else
{
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
}
Extension Methods for Organization
Keep Program.cs clean by organizing registrations into extension methods:
namespace InvenTrack.Api.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRepositories(
this IServiceCollection services)
{
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
return services;
}
public static IServiceCollection AddBusinessServices(
this IServiceCollection services)
{
services.AddScoped<IProductService, ProductService>();
services.AddScoped<IInventoryService, InventoryService>();
services.AddScoped<IOrderService, OrderService>();
return services;
}
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Database
services.AddDbContext<InvenTrackDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
// Caching
services.AddSingleton<ICacheService, RedisCacheService>();
// Email
services.AddTransient<IEmailService, SendGridEmailService>();
return services;
}
}
var builder = WebApplication.CreateBuilder(args);
// Clean, organized registration
builder.Services.AddControllers();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddRepositories();
builder.Services.AddBusinessServices();
var app = builder.Build();
app.MapControllers();
app.Run();
Assembly Scanning
Automatically register services by scanning assemblies (requires a third-party library like Scrutor):
dotnet add package Scrutor
builder.Services.Scan(scan => scan
// Scan the current assembly
.FromAssemblyOf<Program>()
// Register all classes ending with "Service"
.AddClasses(classes => classes.Where(t => t.Name.EndsWith("Service")))
.AsImplementedInterfaces()
.WithScopedLifetime()
// Register all classes ending with "Repository"
.AddClasses(classes => classes.Where(t => t.Name.EndsWith("Repository")))
.AsImplementedInterfaces()
.WithScopedLifetime()
);
This automatically registers:
β’ ProductService as IProductService
β’ ProductRepository as IProductRepository
β’ Any class ending with "Service" or "Repository"
Decorator Pattern
Wrap a service with additional behavior (e.g., logging, caching):
// Original service
public class ProductService : IProductService
{
public async Task<Product?> GetByIdAsync(int id)
{
// Get from database
}
}
// Decorator that adds caching
public class CachedProductService : IProductService
{
private readonly IProductService _inner;
private readonly ICacheService _cache;
public CachedProductService(IProductService inner, ICacheService cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product:{id}";
// Try cache first
var cached = await _cache.GetAsync<Product>(cacheKey);
if (cached != null) return cached;
// Call inner service
var product = await _inner.GetByIdAsync(id);
// Cache result
if (product != null)
{
await _cache.SetAsync(cacheKey, product);
}
return product;
}
}
Registering Decorators with Scrutor
// Register the base service
builder.Services.AddScoped<IProductService, ProductService>();
// Decorate it with caching
builder.Services.Decorate<IProductService, CachedProductService>();
Service Forwarding
Register the same implementation for multiple interfaces:
public class NotificationService : IEmailService, ISmsService
{
public void SendEmail(string to, string message) { }
public void SendSms(string to, string message) { }
}
// Register the concrete type
builder.Services.AddScoped<NotificationService>();
// Forward both interfaces to the same instance
builder.Services.AddScoped<IEmailService>(sp =>
sp.GetRequiredService<NotificationService>());
builder.Services.AddScoped<ISmsService>(sp =>
sp.GetRequiredService<NotificationService>());
Best Practices
1. Organize by Layer
// Infrastructure (Database, Cache, Email)
builder.Services.AddInfrastructure(builder.Configuration);
// Data Access (Repositories)
builder.Services.AddRepositories();
// Business Logic (Services)
builder.Services.AddBusinessServices();
// Presentation (Controllers, Filters)
builder.Services.AddControllers();
2. Use Extension Methods
Keep Program.cs clean and focused. Move registrations to extension methods.
3. Register Interface, Not Implementation
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ProductService>();
4. Validate Registrations
var app = builder.Build();
// Validate all services can be resolved
if (app.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
// Try to resolve critical services
services.GetRequiredService<IProductService>();
services.GetRequiredService<IOrderService>();
}
5. Document Complex Registrations
// Register ProductService with custom retry logic
// MaxRetries is configured in appsettings.json
builder.Services.AddScoped<IProductService>(sp =>
{
var maxRetries = sp.GetRequiredService<IConfiguration>()
.GetValue<int>("ProductService:MaxRetries");
return new ProductService(maxRetries);
});
Key Takeaways
- Basic patterns: Interface β Implementation, Concrete type, Factory, Instance
- Open generics: Register
IRepository<>for any type - Conditional registration: Different services per environment/config
- Extension methods: Organize registrations by layer/feature
- Assembly scanning: Auto-register by convention (Scrutor)
- Decorators: Wrap services with additional behavior
- Service forwarding: Same instance for multiple interfaces
- Keep
Program.csclean and organized - Register interfaces, not implementations (usually)
- Validate critical services can be resolved
- Document complex registrations
You now know how to register services in various ways! In the next section, we'll explore Constructor Injectionβbest practices, handling multiple constructors, optional dependencies, and common patterns for InvenTrack.