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.
IOrderAppServiceCommands / Queries (e.g. MediatR):
CreateOrderCommandHandlerDTOs, 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 servicesMyApp.Application.Handlers– MediatR request handlersMyApp.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
FooServicein theServicesnamespace, 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
FooServiceimplementingIFooServiceinMyApp.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:
AsMatchingInterface: Class
OrderService→ interfaceIOrderServiceby naming convention:services.Scan(scan => scan .FromAssemblies(assembly) .AddClasses() .AsMatchingInterface() .WithScopedLifetime() );Pipeline decorators for MediatR (if you’re not using MediatR’s own behaviors):
You can create a generic decorator around handlers and useDecorateonIRequestHandler<,>.Module-based scanning:
Instead of one bigScan, 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