Register 50 Services in 2 Lines Using Scrutor
Stop writing AddScoped for every service. Use Scrutor's Scan() to auto-register entire assemblies by convention, filter by namespace or interface, and apply the decorator pattern cleanly.
Why Manual Registration Doesn't Scale
A real app has dozens of services. Program.cs turns into a wall of noise:
// This gets out of hand fast
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<ICustomerRepository, SqlCustomerRepository>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
builder.Services.AddScoped<IInventoryRepository, SqlInventoryRepository>();
builder.Services.AddScoped<IShippingService, ShippingService>();
builder.Services.AddScoped<IPaymentService, StripePaymentService>();
builder.Services.AddScoped<IEmailService, SendGridEmailService>();
builder.Services.AddScoped<INotificationService, PushNotificationService>();
// ... 40 more linesEvery new service requires a manual line. Forget one and you get a runtime exception. Scrutor eliminates this with convention-based scanning.
Install Scrutor
dotnet add package ScrutorScrutor adds Scan() and Decorate() extension methods to IServiceCollection. It does not replace the ASP.NET Core container — it auto-populates it.
Basic Scan — Register Everything
// Program.cs
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>() // scan the assembly containing Program
.AddClasses() // find all concrete (non-abstract) classes
.AsImplementedInterfaces() // register as the interfaces they implement
.WithScopedLifetime()); // use Scoped lifetime for all of themThat's it. Every class in the assembly that implements at least one interface is registered as Scoped.
FromAssemblyOf<T>() uses T to locate the assembly. Use any type in the target assembly — Program, a service class, a marker interface.
Scan Multiple Assemblies
builder.Services.Scan(scan => scan
.FromAssembliesOf(
typeof(Program), // OrderFlow.Api
typeof(OrderService), // OrderFlow.Application
typeof(SqlOrderRepository)) // OrderFlow.Infrastructure
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());Filter by Namespace
Only register classes from specific namespaces:
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.InNamespaces(
"OrderFlow.Application.Services",
"OrderFlow.Application.Validators"))
.AsImplementedInterfaces()
.WithScopedLifetime());Exclude namespaces:
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes
.NotInNamespace("OrderFlow.Infrastructure.Migrations"))
.AsImplementedInterfaces()
.WithScopedLifetime());Filter by Interface
Register only classes that implement a specific marker interface:
// Marker interface — no methods needed
public interface IService { }
public interface IRepository { }
// Your classes opt in by implementing the marker
public class OrderService : IOrderService, IService { ... }
public class SqlOrderRepository : IOrderRepository, IRepository { ... }builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<IService>())
.AsImplementedInterfaces()
.WithScopedLifetime());
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<IRepository>())
.AsImplementedInterfaces()
.WithScopedLifetime());Different Lifetimes for Different Groups
Chain multiple scans in one call:
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
// Repositories — Scoped (they wrap DbContext which is Scoped)
.AddClasses(classes => classes.InNamespace("OrderFlow.Infrastructure.Repositories"))
.AsImplementedInterfaces()
.WithScopedLifetime()
// Application services — Scoped
.AddClasses(classes => classes.InNamespace("OrderFlow.Application.Services"))
.AsImplementedInterfaces()
.WithScopedLifetime()
// Validators — Transient (lightweight, stateless)
.AddClasses(classes => classes.InNamespace("OrderFlow.Application.Validators"))
.AsImplementedInterfaces()
.WithTransientLifetime()
// Background workers — Singleton (IHostedService must be singleton)
.AddClasses(classes => classes.AssignableTo<IHostedService>())
.AsImplementedInterfaces()
.WithSingletonLifetime());Register as Self
Sometimes you need both the interface and the concrete type:
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.InNamespace("OrderFlow.Application.Services"))
.AsSelf() // register as the concrete type
.AsImplementedInterfaces() // AND as the interface
.WithScopedLifetime());
// Now both of these work:
sp.GetRequiredService<IOrderService>();
sp.GetRequiredService<OrderService>();Exclude Specific Classes
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes
.InNamespace("OrderFlow.Application.Services")
.WithoutAttribute<ManuallyRegisteredAttribute>()) // skip annotated classes
.AsImplementedInterfaces()
.WithScopedLifetime());
// Classes that need special setup opt out of auto-registration
[ManuallyRegistered]
public class StripePaymentService : IPaymentService
{
// Registered manually with specific config
}
// Program.cs — manual registration for complex setup
builder.Services.AddScoped<IPaymentService, StripePaymentService>(sp =>
{
var opts = sp.GetRequiredService<IOptions<StripeOptions>>();
return new StripePaymentService(opts.Value.ApiKey, opts.Value.WebhookSecret);
});The Decorator Pattern With Decorate()
Add cross-cutting behavior (logging, caching, retries) to existing services without modifying them.
// Original service
public class OrderRepository : IOrderRepository
{
private readonly OrderFlowDbContext _db;
public OrderRepository(OrderFlowDbContext db) => _db = db;
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
=> await _db.Orders.FindAsync(new object[] { id }, ct);
}
// Decorator — adds caching without touching OrderRepository
public class CachedOrderRepository : IOrderRepository
{
private readonly IOrderRepository _inner;
private readonly IMemoryCache _cache;
private readonly ILogger<CachedOrderRepository> _logger;
public CachedOrderRepository(
IOrderRepository inner,
IMemoryCache cache,
ILogger<CachedOrderRepository> logger)
{
_inner = inner;
_cache = cache;
_logger = logger;
}
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
var key = $"order:{id}";
if (_cache.TryGetValue(key, out Order? cached))
{
_logger.LogDebug("Cache hit for order {Id}", id);
return cached;
}
var order = await _inner.GetByIdAsync(id, ct);
if (order is not null)
_cache.Set(key, order, TimeSpan.FromMinutes(5));
return order;
}
}// Program.cs — register original, then decorate
builder.Services.AddMemoryCache();
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());
// Wrap IOrderRepository with the caching decorator
builder.Services.Decorate<IOrderRepository, CachedOrderRepository>();
// The container now resolves:
// IOrderRepository -> CachedOrderRepository(inner: OrderRepository)Stack multiple decorators (applied outside-in):
builder.Services.Decorate<IOrderRepository, CachedOrderRepository>(); // outermost
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>(); // middle
// Inner: OrderRepository (original)
// Call: LoggingOrderRepository -> CachedOrderRepository -> OrderRepositoryComplete Program.cs Setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddMemoryCache();
// DbContext — registered before Scan so repositories can inject it
builder.Services.AddDbContext<OrderFlowDbContext>(opts =>
opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Options
builder.Services
.AddOptions<AppOptions>()
.BindConfiguration(AppOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
// Auto-register all services, repositories, validators
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(Program), typeof(OrderService), typeof(SqlOrderRepository))
.AddClasses(classes => classes.InNamespaceOf<OrderService>())
.AsImplementedInterfaces()
.WithScopedLifetime()
.AddClasses(classes => classes.InNamespaceOf<SqlOrderRepository>())
.AsImplementedInterfaces()
.WithScopedLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IValidator<>)))
.AsImplementedInterfaces()
.WithTransientLifetime());
// Decorators
builder.Services.Decorate<IOrderRepository, CachedOrderRepository>();
var app = builder.Build();
app.MapControllers();
app.Run();When NOT to Use Scrutor
Auto-registration by convention is powerful but has limits:
- Services that need complex factory setup (use manual
AddScopedwith a factory lambda) - Keyed services (use
AddKeyedScoped— Scrutor does not support keyed registration) - Services with multiple implementations of the same interface (use keyed services or explicit registration)
- Third-party library services (
AddHttpClient,AddDbContext, etc.)
What to Learn Next
- Middleware Pipeline: Add cross-cutting behavior at the HTTP level instead of the service level
- Keyed Services: Handle multiple implementations of the same interface
- Clean Architecture: Structure your assembly namespaces so Scrutor scans work cleanly
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.