HomeNikola Knezevic

In this article

Banner

Better Dependency Injection with Scrutor in ASP.NET Core

23 Oct 2025
8 min

Sponsor Newsletter

Dependency Injection (DI) is a cornerstone of ASP.NET Core applications.

Dependency Injection is a software design pattern that implements the Inversion of Control (IOC) principle, allowing objects to receive their dependencies from the outside instead of creating them themselves.

While the built-in DI container is powerful, managing registrations manually can become repetitive and error-prone, especially as projects grow.

That’s where Scrutor comes in extending Microsoft.Extensions.DependencyInjection package.

Scrutor

In ASP.NET Core, every service you register has a lifetime that controls how long an instance of the service lives. There are three main lifetimes:

  • Transient - A new instance is created every time the service is requested.
  • Scoped - One instance is created per HTTP request.
  • Singleton - A single instance is created once for the entire application lifetime.

As your application grows, the number of services you create increases, and every time you have to manually register them in the DI container. Mistakes and forgotten registrations become more frequent, especially as the service count keeps rising.

And don’t even get me started on how cumbersome service decorations can get, you’ll see exactly what I mean later in this blog.

This is where Scrutor comes in.

Scrutor is an open-source library for ASP.NET Core that extends the default DI container. It allows developers to:

  • Scan assemblies and automatically register services.
  • Decorate services without modifying existing registrations.
  • Reduce boilerplate code for DI configuration.

In short, Scrutor lets you write less code while achieving more, keeping your DI setup clean, maintainable and scalable.

By the way, I know what some of you might be thinking: “Does Scrutor use reflection?”

The answer is yes. In recent years, there’s been a trend to avoid reflection, but since this only happens at application startup, it’s not a big deal. Most likely, nobody will notice any performance difference, so there’s no need to worry.

Getting Started

To get started with Scrutor, you'll first need to install the necessary NuGet packages. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:

shell
Install-Package Scrutor

Once the package is added, your IServiceCollection gains two powerful extension methods:

  • Scan
  • Decorate

Scan

Instead of manually registering services like this:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IMyService, MyService>();
builder.Services.AddTransient<IOtherService, OtherService>();
builder.Services.AddTransient<IAnotherService, AnotherService>();
// …and many more

You can let Scrutor handle it automatically. With Scrutor, you can scan assemblies and register all your services in just a few lines, drastically reducing boilerplate and the chance of forgetting a registration:

csharp
builder.Services.Scan(scan => scan
    .FromAssemblyOf<Program>()
    .AddClasses()
    .AsImplementedInterfaces()
    .WithTransientLifetime());

This will automatically find all classes in the assembly that implement interfaces and register them with a transient lifetime.

In my applications, I use a marker interface approach to assign appropriate lifetimes consistently.

  • Create marker interfaces for each lifetime:

    csharp
    public interface ITransient { }
    public interface IScoped { }
    public interface ISingleton { }
  • Add them to your interfaces:

    csharp
    public interface IMyService : IScoped
    {
        // ...
    }
    
    public sealed class MyService : IMyService
    {
        // ...
    }
    
  • Create extension methods to register services by lifetime:

    csharp
    internal static class ServiceCollectionExtensions
    {
        internal static IServiceCollection AddTransientServicesAsMatchingInterfaces(
            this IServiceCollection services, Assembly assembly) =>
            services.Scan(scan =>
                scan.FromAssemblies(assembly)
                    .AddClasses(filter => filter.AssignableTo<ITransient>(), false)
                    .UsingRegistrationStrategy(RegistrationStrategy.Throw)
                    .AsMatchingInterface()
                    .WithTransientLifetime());
    
        internal static IServiceCollection AddScopedServicesAsMatchingInterfaces(
            this IServiceCollection services, Assembly assembly) =>
            services.Scan(scan =>
                scan.FromAssemblies(assembly)
                    .AddClasses(filter => filter.AssignableTo<IScoped>(), false)
                    .UsingRegistrationStrategy(RegistrationStrategy.Throw)
                    .AsMatchingInterface()
                    .WithScopedLifetime());
    
        internal static IServiceCollection AddSingletonServicesAsMatchingInterfaces(
            this IServiceCollection services, Assembly assembly) =>
            services.Scan(scan =>
                scan.FromAssemblies(assembly)
                    .AddClasses(filter => filter.AssignableTo<ISingleton>(), false)
                    .UsingRegistrationStrategy(RegistrationStrategy.Throw)
                    .AsMatchingInterface()
                    .WithSingletonLifetime());
    }
  • Call the extension methods at startup:

    csharp
    builder.Services
        .AddTransientServicesAsMatchingInterfaces(assembly)
        .AddScopedServicesAsMatchingInterfaces(assembly)
        .AddSingletonServicesAsMatchingInterfaces(assembly);

And just like that, all your services are registered automatically with the correct lifetime, no more manual errors or forgotten registrations.

Decorate

The Decorator Pattern is a structural design pattern that allows you to add behavior to objects dynamically without modifying their original implementation.

In short, it's a way to "wrap" a service with additional functionality, something like middlewares for services.

Here's an example of service decoration without Scrutor:

csharp
services.AddScoped<IMyService, MyService>();
services.AddScoped<IMyService>(provider =>
{
    var original = provider.GetRequiredService<MyService>();
    return new LoggingMyService(original);
});

It works fine, but it’s just a single decorator. Now, with Scrutor, it becomes much cleaner:

csharp
builder.Services.Decorate<IMyService, LoggingMyService>();

Now imagine having multiple decorators manually, things can get messy quickly:

csharp
services.AddScoped<IMyService, MyService>();
services.AddScoped<IMyService>(provider =>
{
    var original = provider.GetRequiredService<MyService>();
    return new LoggingMyService(original);
});
services.AddScoped<IMyService>(provider =>
{
    var previous = provider.GetRequiredService<MyService>();
    return new CachingMyService(previous);
});

This is exactly where Scrutor shines:

csharp
builder.Services.AddScoped<IMyService, MyService>()
    .Decorate<IMyService, LoggingMyService>()
    .Decorate<IMyService, CachingMyService>();

Everything stays readable, and adding new decorators is just one line, no lambda boilerplate required.

To learn more about decorator pattern check my full blog post on them: Decorator Design Pattern in ASP.NET Core

NOTE: Order matters, the first decorator wraps the original service, the next wraps the previous decorator and so on.

Did You Know?

Whenever I write about a topic, I always prepare thoroughly and do some mini research to make sure I cover all the important aspects.

One interesting thing I discovered is that one of the biggest sponsors of this project is ZZZ Projects. They also happen to be my sponsors, and their support helps keep this package and my work maintained and evolving, benefiting the entire ASP.NET Core community.

So, everyone go check out ZZZ Projects and Scrutor, give them a star, show some support, and give it a chance, it might just change the way you build apps in .NET!

Conclusion

Scrutor extends the default DI container, allowing developers to scan assemblies, automatically register services, and decorate them cleanly.

By reducing boilerplate and keeping lifetimes consistent, Scrutor helps maintain a scalable, maintainable, and readable DI setup.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.