Back to blog
Backend Systemsintermediate

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.

LearnixoApril 14, 20265 min read
.NETC#ASP.NET CoreDependency InjectionScrutor
Share:𝕏

Why Manual Registration Doesn't Scale

A real app has dozens of services. Program.cs turns into a wall of noise:

C#
// 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 lines

Every new service requires a manual line. Forget one and you get a runtime exception. Scrutor eliminates this with convention-based scanning.


Install Scrutor

Bash
dotnet add package Scrutor

Scrutor 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

C#
// 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 them

That'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

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

C#
builder.Services.Scan(scan => scan
    .FromAssemblyOf<Program>()
    .AddClasses(classes => classes.InNamespaces(
        "OrderFlow.Application.Services",
        "OrderFlow.Application.Validators"))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

Exclude namespaces:

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

C#
// 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 { ... }
C#
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:

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

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

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

C#
// 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;
    }
}
C#
// 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):

C#
builder.Services.Decorate<IOrderRepository, CachedOrderRepository>();    // outermost
builder.Services.Decorate<IOrderRepository, LoggingOrderRepository>();   // middle
// Inner: OrderRepository (original)
// Call: LoggingOrderRepository -> CachedOrderRepository -> OrderRepository

Complete Program.cs Setup

C#
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 AddScoped with 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?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.