Dependency Injection Using Scrutor

Scrutor is basically “DI on rails” for Microsoft.Extensions.DependencyInjection:

  • It gives you assembly scanning so you don’t manually call AddScoped<IMyService, MyService>() 500 times.

  • It gives you decorators so you can add logging, caching, retries, etc. around your interfaces in a clean, testable way.

That’s exactly what you want in a DDD / Clean Architecture solution, where you’ve got multiple layers and tons of small services/handlers.

1. Quick mental model of Scrutor

Scrutor extends IServiceCollection with:

a) Scan(...) – assembly scanning

You tell it:

  • Where to look: FromAssemblyOf<T>(), FromAssemblies(...), FromApplicationDependencies(...).

  • What to include: .AddClasses(c => c.AssignableTo<ISomeInterface>() / InNamespaces(...) / Where(...))

  • How to expose: .AsSelf(), .AsImplementedInterfaces(), .AsMatchingInterface(), etc.

  • Lifetime: .WithScopedLifetime(), .WithTransientLifetime(), .WithSingletonLifetime().

Example:

services.Scan(scan => scan
    .FromAssemblyOf<ISomeApplicationService>()
        .AddClasses(c => c.InNamespaces("MyApp.Application.Services"))
            .AsImplementedInterfaces()
            .WithScopedLifetime()
);

b) Decorate<TService, TDecorator>() – decorators

You apply cross-cutting behavior around an existing registration:

services.AddScoped<IOrderService, OrderService>();

services.Decorate<IOrderService, LoggingOrderServiceDecorator>();
services.Decorate<IOrderService, CachingOrderServiceDecorator>();

Final chain is:

CachingDecorator(LoggingDecorator(OrderService))

Order of Decorate calls = order of wrapping.

2. Typical Clean Architecture / DDD solution structure

Let’s assume a classic layout:

  • MyApp.Domain

    • Entities, ValueObjects, Aggregates

    • Domain Events

    • Interfaces like IUnitOfWork, domain abstractions (if any)

  • MyApp.Application

    • Use Cases (Application Services): e.g. IOrderAppService

    • Commands / Queries (e.g. MediatR): CreateOrderCommandHandler

    • DTOs, Validators, Mapping profiles

    • Interfaces like ICurrentUserService, IEmailSender (implemented in Infra)

  • MyApp.Infrastructure

    • EF Core DbContext, EfRepository<T>

    • External service implementations (email, queues, storage)

    • Logging / integrations

  • MyApp.WebApi (or Web / Blazor)

    • Program.cs / Startup

    • Controllers / Endpoints

    • UI / API plumbing

Scrutor fits in primarily at the Application and Infrastructure layers (for registrations and decorators), with WebApi calling into “AddApplication” / “AddInfrastructure” extension methods.

3. Example: Application layer wiring with Scrutor

3.1 Application marker & folders

In MyApp.Application create a marker type:

namespace MyApp.Application;

public sealed class ApplicationAssemblyMarker { }

Suppose you have:

  • MyApp.Application.Services – use-case services

  • MyApp.Application.Handlers – MediatR request handlers

  • MyApp.Application.Validation – FluentValidation validators

Now create a DI extension:

MyApp.Application/DependencyInjection.cs

using Microsoft.Extensions.DependencyInjection;
using MyApp.Application;
using FluentValidation;

namespace Microsoft.Extensions.DependencyInjection;

public static class ApplicationServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationLayer(this IServiceCollection services)
    {
        var assembly = typeof(ApplicationAssemblyMarker).Assembly;

        services.Scan(scan => scan
            .FromAssemblies(assembly)

            // 1. Application services (e.g., *Service classes)
            .AddClasses(c => c
                .InNamespaces("MyApp.Application.Services")
                .Where(t => t.Name.EndsWith("Service")))
                .AsImplementedInterfaces()
                .WithScopedLifetime()

            // 2. MediatR handlers, if you use them
            .AddClasses(c => c
                .InNamespaces("MyApp.Application.Handlers")
                .Where(t => t.Name.EndsWith("Handler")))
                .AsImplementedInterfaces()
                .WithScopedLifetime()

            // 3. FluentValidation validators
            .AddClasses(c => c.AssignableTo(typeof(IValidator<>)))
                .AsImplementedInterfaces()
                .WithTransientLifetime()
        );

        // ... any other Application-specific registrations (MediatR, AutoMapper, etc.)

        return services;
    }
}

This gives you:

  • No manual AddScoped<ICreateOrderService, CreateOrderService>().

  • You just create a new FooService in the Services namespace, and DI picks it up automatically.

4. Infrastructure layer wiring with Scrutor (+ decorators)

4.1 Domain & Application abstractions

In Domain:

namespace MyApp.Domain.Abstractions;

public interface IRepository<TAggregate>
    where TAggregate : class
{
    Task<TAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
    // ... etc
}

In Application you depend only on IRepository<T> and other abstractions.

4.2 EF implementation in Infrastructure

In Infrastructure:

using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Abstractions;

namespace MyApp.Infrastructure.Persistence;

public sealed class EfRepository<TAggregate> : IRepository<TAggregate>
    where TAggregate : class
{
    private readonly AppDbContext _dbContext;

    public EfRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task<TAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
        => _dbContext.Set<TAggregate>().FindAsync(new object[] { id }, cancellationToken).AsTask();

    public Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default)
    {
        _dbContext.Set<TAggregate>().Add(aggregate);
        return Task.CompletedTask;
    }
}

4.3 Infrastructure marker & decorator

Marker:

namespace MyApp.Infrastructure;

public sealed class InfrastructureAssemblyMarker { }

Logging decorator for all repositories:

using MyApp.Domain.Abstractions;
using Microsoft.Extensions.Logging;

namespace MyApp.Infrastructure.Decorators;

public sealed class RepositoryLoggingDecorator<TAggregate> : IRepository<TAggregate>
    where TAggregate : class
{
    private readonly IRepository<TAggregate> _inner;
    private readonly ILogger<RepositoryLoggingDecorator<TAggregate>> _logger;

    public RepositoryLoggingDecorator(
        IRepository<TAggregate> inner,
        ILogger<RepositoryLoggingDecorator<TAggregate>> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task<TAggregate?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Getting {Aggregate} with Id {Id}", typeof(TAggregate).Name, id);
        var aggregate = await _inner.GetByIdAsync(id, cancellationToken);
        return aggregate;
    }

    public Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Adding {Aggregate}", typeof(TAggregate).Name);
        return _inner.AddAsync(aggregate, cancellationToken);
    }
}

4.4 Infrastructure DI extension with Scrutor

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Domain.Abstractions;
using MyApp.Infrastructure;
using MyApp.Infrastructure.Persistence;
using MyApp.Infrastructure.Decorators;

namespace Microsoft.Extensions.DependencyInjection;

public static class InfrastructureServiceCollectionExtensions
{
    public static IServiceCollection AddInfrastructureLayer(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var assembly = typeof(InfrastructureAssemblyMarker).Assembly;

        // DbContext
        services.AddDbContext<AppDbContext>(options =>
        {
            options.UseSqlServer(configuration.GetConnectionString("Default"));
        });

        // Scrutor scanning for Infra implementations
        services.Scan(scan => scan
            .FromAssemblies(assembly)

            // EF repositories, e.g. any class implementing IRepository<>
            .AddClasses(c => c.AssignableTo(typeof(IRepository<>)))
                .AsImplementedInterfaces()
                .WithScopedLifetime()

            // External services (e.g. email, queue) by namespace
            .AddClasses(c => c.InNamespaces("MyApp.Infrastructure.ExternalServices"))
                .AsImplementedInterfaces()
                .WithScopedLifetime()
        );

        // Decorate the open generic IRepository<>
        services.Decorate(typeof(IRepository<>), typeof(RepositoryLoggingDecorator<>));

        return services;
    }
}

Now your Application layer code uses IRepository<T> and doesn’t know or care that:

  • It’s EF under the hood.

  • There’s logging around it via RepositoryLoggingDecorator<>.

5. Putting it all together in WebApi / Blazor Server (Program.cs)

In your top-level project, e.g. MyApp.WebApi:

using MyApp.Application;
using MyApp.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

// Add layers via DI extensions
builder.Services
    .AddApplicationLayer()
    .AddInfrastructureLayer(builder.Configuration);

// Controllers / minimal APIs / Razor / Blazor etc.
builder.Services.AddControllers();
// builder.Services.AddRazorPages();
// builder.Services.AddServerSideBlazor();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();

app.Run();

From here on:

  • To add a new Application service:

    • Create FooService implementing IFooService in MyApp.Application.Services.

    • Scrutor picks it up automatically.

  • To add a new Infrastructure service:

    • Implement the corresponding interface in a proper namespace (e.g., MyApp.Infrastructure.ExternalServices).

    • Scrutor registers it automatically.

  • To add cross-cutting behavior:

    • Write a decorator and apply services.Decorate<...>() in the relevant layer’s DI extension.

6. Variations you might like

A few extra common patterns:

  1. AsMatchingInterface: Class OrderService → interface IOrderService by naming convention:

    services.Scan(scan => scan
        .FromAssemblies(assembly)
            .AddClasses()
                .AsMatchingInterface()
                .WithScopedLifetime()
    );
    
  2. Pipeline decorators for MediatR (if you’re not using MediatR’s own behaviors):
    You can create a generic decorator around handlers and use Decorate on IRequestHandler<,>.

  3. Module-based scanning:
    Instead of one big Scan, you can have separate extension methods per feature:
    AddBillingModule, AddSchedulingModule, each using Scrutor to register its own services.

If you tell me your exact project names/namespaces (e.g. Meditransact.* or EagleSwift.Scheduler.*), I can tailor the Scrutor Scan calls and decorators to match your real solution and even wire in MediatR, Serilog, and Radzen-related services the way you like to structure them.

#dotnet #csharp #dotnet10

Previous
Previous

.Net 10 project files explained

Next
Next

Understanding TryGetNonEnumeratedCount in C#