.NET  

Scrutor Common Dependency Injection Pitfalls and How to Avoid Them

Scrutor is a powerful extension to the built-in Dependency Injection (DI) container in .NET. It enables assembly scanning, convention-based registration, and decorator support without replacing the default DI system.

While Scrutor significantly reduces boilerplate, it also introduces indirection that can hide architectural problems if used without discipline. Most Dependency Injection issues seen in real-world applications are not caused by Scrutor itself, but by overly broad scanning rules, unclear lifetimes, or implicit registrations that are difficult to reason about.

This article explores the most common Dependency Injection pitfalls when using Scrutor and explains how to avoid them using practical, production-oriented examples.

1. Over-Scanning Assemblies

The problem

builder.Services.Scan(scan =>
    scan.FromApplicationDependencies()
        .AddClasses()
        .AsImplementedInterfaces()
);
  

Why does this cause issues

Scanning all application dependencies registers every discoverable class, including framework and third-party types. This makes the DI container unpredictable, slows application startup, and creates behavior that is difficult to debug and reason about.

The correct approach

Scan only assemblies you own and apply strict filters.

  
    builder.Services.Scan(scan =>
    scan.FromAssemblyOf<IApplicationService>()
        .AddClasses(c => c.AssignableTo<IApplicationService>())
        .AsImplementedInterfaces()
        .WithScopedLifetime()
);
  

Assembly scanning should always be deliberate and narrowly scoped.

2. Accidental Multiple Interface Registrations

The problem

public class UserService :
    IUserService,
    IApplicationService,
    IDisposable
{
} 
.AsImplementedInterfaces() 

This registers every implemented interface, including interfaces that were never meant to be resolved from the container.

Why this is dangerous

Unintended registrations can override other services and introduce subtle bugs that appear only at runtime.

The correct approach

Be explicit when registering services that implement multiple interfaces.

.AddClasses(c => c.AssignableTo<IApplicationService>())
.As<IUserService>()
.WithScopedLifetime()
  

Automatic interface registration should be used only when the intent is clear and controlled.

3. Lifetime Mismatch (Captive Dependency)

The problem

services.AddSingleton<ReportService>();
services.AddScoped<AppDbContext>();
  
public class ReportService
{
    public ReportService(AppDbContext context) { }
}
  

Why this breaks applications

A singleton capturing a scoped dependency leads to memory leaks, invalid object lifetimes, and runtime exceptions under load.

The correct approach

Align lifetimes so that dependencies live at least as long as their consumers.

services.AddScoped<ReportService>();

Alternatively, use factory abstractions such as IDbContextFactory .

4. Hidden Decorators and Execution Order Confusion

The problem

services.Decorate<IUserService, LoggingUserService>();
services.Decorate<IUserService, CachingUserService>();
  

What actually happens

The runtime execution order becomes:

Caching -> Logging -> UserService 

Why this is confusing

Decorators are invisible at injection sites, and the execution order is not obvious. Changing the registration order can silently alter application behavior.

The correct approach

Register decorators together, document their order, and add constructor logging in development to make resolution explicit.

public LoggingUserService(...)
{
    Console.WriteLine("Logging decorator created");
}
  

Decorator order should always be intentional.

5. Mixing Manual and Scrutor Registrations

The problem

services.AddScoped<IUserService, UserService>();

services.Scan(scan =>
    scan.FromAssemblyOf<IApplicationService>()
        .AddClasses(c => c.AssignableTo<IApplicationService>())
        .AsImplementedInterfaces()
);
  

Why this leads to bugs

The .NET DI container follows a last-registration-wins strategy. Mixing manual and scanned registrations makes behavior unpredictable and environment-dependent.

The correct approach

Use one registration strategy per layer. Scrutor is well-suited for application services and repositories, while manual registration should be reserved for infrastructure and framework services.

Consistency is more important than flexibility.

6. Not Validating Dependency Injection at Startup

The problem

Dependency Injection errors surface only at runtime, often under load.

The correct approach

Enable DI validation during application startup.

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;
    options.ValidateOnBuild = true;
});
  

This configuration catches missing registrations, circular dependencies, and lifetime mismatches early.

7. Business Logic Inside Decorators

The problem

public class AuthorizationDecorator : IUserService
{
    public async Task DeleteUser(int id)
    {
        if (!IsAdmin())
            throw new UnauthorizedAccessException();

        await _inner.DeleteUser(id);
    }
}
  

Why this is a design flaw

Decorators should not contain business rules. Hiding business logic inside decorators makes behavior harder to understand, test, and maintain.

The correct approach

Decorators should handle technical cross-cutting concerns such as logging, caching, validation, or metrics. Business rules should remain in the core service.

8. Overusing Singleton Services

The problem

services.AddSingleton<UserService>();

Why this causes problems

Singletons introduce shared mutable state, thread-safety issues, and test instability. Many services do not need to live for the entire lifetime of the application.

The correct approach

Default to scoped services unless there is a clear and justified reason to use a singleton.

services.AddScoped<IUserService, UserService>(); 

Singletons should be rare and stateless.

9. Service Locator Usage in Minimal APIs

The problem

app.MapGet("/users", (IServiceProvider provider) =>
{
    var service = provider.GetRequiredService<IUserService>();
});
  

Why this is harmful

Manually resolving services hides dependencies and reintroduces the service locator anti-pattern, making the code harder to test and reason about.

The correct approach

Let the framework inject dependencies directly.

app.MapGet("/users", (IUserService service) =>
{
    return service.GetAll();
});
  

Mental Model to Remember

Scrutor reduces boilerplate, but it also hides complexity. If your DI configuration is difficult to explain, difficult to debug, or behaves differently across environments, your scanning rules are too permissive.

Key Takeaway

Scrutor is not inherently dangerous. Uncontrolled conventions are.

When used with tight assembly scanning, clear lifetimes, explicit intent, and proper validation, Scrutor becomes a powerful tool for building clean, scalable Dependency Injection in .NET.

Happy Coding!

I write about modern C#, .NET, and real-world development practices. Follow me on C# Corner for regular insights, tips, and deep dives.