Section 4 of 6

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

Standard Pattern C#
builder.Services.AddScoped<IProductService, ProductService>();

Request IProductService, get ProductService.

2. Concrete Type Only

No Interface C#
builder.Services.AddScoped<ProductService>();

Request ProductService directly (no abstraction).

3. Factory Function

Custom Creation Logic C#
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

Pre-created Instance C#
var cacheService = new RedisCacheService("localhost:6379");
builder.Services.AddSingleton<ICacheService>(cacheService);

Always registered as Singleton (instance already exists).

Generic Type Registration

Closed Generic Types

Specific Generic Types C#
// 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:

Open Generics C#
// 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.
πŸ’‘ Open Generics Benefits

One registration works for all types! No need to register IRepository<Product>, IRepository<Customer>, etc. separately.

Conditional Registration

Environment-Based Registration

Different Services per Environment C#
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

Based on App Settings C#
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:

Extensions/ServiceCollectionExtensions.cs C#
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;
    }
}
Program.cs (Clean!) C#
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):

Terminal Shell
dotnet add package Scrutor
Auto-Registration with Scrutor C#
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()
);
ℹ️ Convention-Based Registration

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):

Decorator Example C#
// 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

Decorator Registration C#
// 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:

Multiple Interfaces C#
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

Layered Organization C#
// 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

βœ… Good C#
builder.Services.AddScoped<IProductService, ProductService>();
❌ Bad (usually) C#
builder.Services.AddScoped<ProductService>();

4. Validate Registrations

Validation C#
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

Comments for Clarity C#
// 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.cs clean and organized
  • Register interfaces, not implementations (usually)
  • Validate critical services can be resolved
  • Document complex registrations
🎯 Next Steps

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.