CodeToClarity Logo
Published on ·.NET

Automate Your .NET DI Setup with Scrutor (Step-by-Step Guide)

Kishan KumarKishan Kumar

Learn how to use Scrutor in .NET to automatically register dependencies and simplify your DI setup. This step-by-step guide covers setup, lifetimes, conventions, and best practices for clean architecture.

If you’ve been working with Dependency Injection (DI) in .NET for a while, you’ve probably felt the pain — your Program.cs keeps growing, and you’re stuck writing the same service registrations over and over.

At first, it feels manageable:

builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();

But as your app scales (especially with Clean Architecture), your DI setup turns into a 300-line monster — repetitive, error-prone, and hard to maintain.

That’s where Scrutor comes to the rescue. 🚀

Scrutor takes .NET’s built-in DI container and supercharges it with assembly scanning and convention-based registration. In simple terms, it automatically finds and registers your services — no manual mapping needed.

Let’s dive in and see how Scrutor helps you clean up your DI setup, scale effortlessly, and build smarter architecture.


💡 Why Manual DI Registration Doesn’t Scale

Manually adding every service is fine for small apps. But once your project grows, this approach:

  • Becomes repetitive and noisy
  • Breaks the Open/Closed Principle (you keep editing startup code)
  • Slows down development when refactoring services
  • Leads to runtime errors when you forget to register something

You’ve probably seen this at least once:

InvalidOperationException: Unable to resolve service for type 'IYourService'

With Scrutor, you can scan entire assemblies, apply filters, and let it auto-register everything that fits your conventions — keeping your DI clean, consistent, and easy to scale.


⚙️ What Is Scrutor?

Scrutor is a lightweight, open-source library built on top of .NET’s default DI container (Microsoft.Extensions.DependencyInjection).

It extends DI with a fluent API that supports:

  • ✅ Assembly scanning
  • ✅ Convention-based registration
  • ✅ Custom filters
  • ✅ Lifetime control (Scoped, Transient, Singleton)

You don’t have to switch to another DI framework — Scrutor plugs right into the existing one.


✨ Example: Without Scrutor vs. With Scrutor

Without Scrutor:

builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<ILoggingService, LoggingService>();

With Scrutor:

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

Scrutor automatically detects and registers all matching classes.
That means fewer lines, fewer mistakes, and more focus on business logic.


🚀 Installing Scrutor in Your .NET Project

Scrutor is available on NuGet and installs in seconds.

Step 1 – Install via CLI:

dotnet add package Scrutor

or via Package Manager:

Install-Package Scrutor

Step 2 – Add the using directive:

using Scrutor;

That’s it — you’re ready to start scanning and registering services!


🔍 Basic Usage: Scan and Register by Convention

Let’s say you have multiple service pairs like:

public interface IUserService { }
public class UserService : IUserService { }

public interface IEmailService { }
public class EmailService : IEmailService { }

Instead of manually registering each one, use Scrutor:

builder.Services.Scan(scan => scan
    .FromAssemblyOf<IUserService>()
    .AddClasses(classes => classes.Where(t => t.Name.EndsWith("Service")))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

What happens here:

  • FromAssemblyOf<T> → Scans the specified assembly
  • AddClasses(...) → Selects classes ending with Service
  • AsImplementedInterfaces() → Maps them to their interfaces
  • WithScopedLifetime() → Registers them as scoped dependencies

Now every new *Service class you add will be automatically picked up — no more DI updates needed.


🔄 Controlling Lifetimes: Scoped, Transient, Singleton

Scrutor fully supports all standard DI lifetimes:

Scoped (default)

.WithScopedLifetime();

Creates one instance per request.

Transient

.WithTransientLifetime();

Creates a new instance every time it’s injected.

Singleton

.WithSingletonLifetime();

Creates a single instance that lives for the entire application lifecycle.

You can even mix lifetimes for different types:

builder.Services.Scan(scan => scan
    .FromAssemblyOf<IService>()
    .AddClasses(c => c.AssignableTo<ICacheService>())
        .AsImplementedInterfaces()
        .WithSingletonLifetime()
    .AddClasses(c => c.AssignableTo<IRequestHandler>())
        .AsImplementedInterfaces()
        .WithScopedLifetime());

🎯 Filtering Registrations with Custom Rules

Scrutor gives you fine-grained control over what gets registered.

Filter by Namespace

.AddClasses(c => c.InNamespace("MyApp.Application.Services"))

Filter by Naming Convention

.AddClasses(c => c.Where(t => t.Name.EndsWith("Service")))

Exclude Specific Types

.AddClasses(c => c.Where(t => t.Name.EndsWith("Service") && t != typeof(EmailService)))

Filter by Interface

.AddClasses(c => c.AssignableTo<IBaseService>())

These filters help prevent over-registration and maintain clean architecture boundaries.


🧱 Attribute-Based Registration

You can also control registration using attributes.

Step 1 – Create an attribute:

[AttributeUsage(AttributeTargets.Class)]
public class InjectableAttribute : Attribute { }

Step 2 – Apply it:

[Injectable]
public class AuditService : IAuditService { }

Step 3 – Register with attribute filter:

.AddClasses(c => c.WithAttribute<InjectableAttribute>())

Only marked classes get registered — great for shared libraries or fine-grained control.


🧩 Scrutor in Clean Architecture

In Clean Architecture, your app is divided into layers:

  • Core (Domain, Application)
  • Infrastructure
  • API/Web

Manually registering services across all layers becomes tedious. Scrutor keeps it clean.

Application Layer

builder.Services.Scan(scan => scan
    .FromAssemblyOf<IUserService>()
    .AddClasses(c => c.Where(t => t.Name.EndsWith("Service")))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

Infrastructure Layer

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

Benefits:

  • No cross-layer leakage
  • Consistent conventions
  • Easy scalability
  • Reduced boilerplate
  • Better testability

🧠 Best Practices for Using Scrutor

  1. Be explicit with filters – Don’t scan everything blindly.
  2. Scan per layer – Keep boundaries clear (Application, Infrastructure, etc.).
  3. Avoid duplicates – Tighten filters or use TryAdd*() if needed.
  4. Organize registrations – Group scans by concern.
  5. Use attributes only when necessary – Keep conventions as your main driver.
  6. Document your conventions – Helps new developers understand your setup.
  7. Don’t scan external assemblies – Focus on your codebase only.
  8. Profile startup performance – For large apps, scanning can add milliseconds.

✅ Conclusion

Scrutor turns your messy DI setup into something clean, scalable, and future-proof.
By using assembly scanning and conventions, it eliminates repetitive boilerplate while keeping your architecture solid and maintainable.

Whether you’re building a small web API or a large enterprise solution, Scrutor helps you focus on business logic, not service registration.

Keep your Program.cs lean.
Let Scrutor do the heavy lifting. 💪