FREE .NET Web API Course! Join Now 🚀

8 min read

Scrutor in .NET – Auto-Register Dependencies for Cleaner and Scalable DI

#dotnet .NET Web API Zero to Hero Course

Dependency Injection in .NET is powerful—but as your project grows, so does your Program.cs. Manually registering every single service gets repetitive, noisy, and error-prone. Especially in large, modular applications following Clean Architecture, you’ll end up with hundreds of lines just for wiring up dependencies.

That’s where Scrutor comes in.

Scrutor extends .NET’s built-in DI container with assembly scanning and convention-based registration. Instead of explicitly mapping each interface to its implementation, you can scan entire assemblies and let Scrutor auto-register your services based on naming conventions, attributes, or custom filters.

In this guide, we’ll explore how to use Scrutor to automate and clean up your DI setup. We’ll cover basic and advanced use cases, lifetimes, best practices, and how it fits into a scalable architecture. If you’re looking to make your DI smarter and your startup code leaner, this one’s for you.

Why Manual DI Registration Doesn’t Scale

Manually registering services like this:

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

works fine—until it doesn’t.

As your project grows, your Program.cs or Startup.cs becomes bloated with repetitive DI lines. It gets worse with Clean Architecture, where each layer (Application, Infrastructure, etc.) has its own set of services and interfaces. You end up copy-pasting DI code across features, increasing the risk of human error and missed registrations.

This manual approach:

  • Doesn’t scale well for large codebases
  • Breaks the open/closed principle when you constantly modify startup files
  • Slows down development when adding or refactoring services
  • Makes it hard to enforce consistency in how services are registered

You might even forget to register a new service entirely, leading to runtime exceptions like:

InvalidOperationException: Unable to resolve service for type 'IYourService' while attempting to activate 'YourController'.

Scrutor solves this by scanning assemblies and auto-registering services based on patterns—freeing you from the boilerplate and keeping your DI clean, consistent, and maintainable.

What is Scrutor?

Scrutor is a lightweight open-source library that extends .NET’s built-in Dependency Injection container with assembly scanning and convention-based registration. It helps you automatically discover and register services without manually mapping each interface to its implementation.

It’s built on top of the default Microsoft.Extensions.DependencyInjection, so no need to switch containers or learn a new DI framework. Scrutor adds a fluent API that lets you scan assemblies, match classes to interfaces, and control lifetimes—all with a few lines of code.

Example:

Instead of doing this manually:

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

With Scrutor, you write:

builder.Services.Scan(scan => scan
.FromAssemblyOf<IUserService>() // or any known type
.AddClasses(classes => classes.AssignableTo<IUserService>())
.AsImplementedInterfaces()
.WithScopedLifetime());

Scrutor finds all matching classes and auto-registers them. It supports:

  • Interface-to-class matching
  • Namespace filtering
  • Lifetime control (Scoped, Transient, Singleton)
  • Attribute-based registration
  • Custom filtering logic

It’s especially useful in Clean Architecture setups where services are spread across multiple layers. With Scrutor, your DI stays DRY, testable, and scalable—without giving up control.

Installing Scrutor in Your .NET Project

Getting started with Scrutor is quick and straightforward. It’s available as a NuGet package and works seamlessly with the built-in DI container.

Step 1: Install via NuGet

Using CLI:

Terminal window
dotnet add package Scrutor

Or via Package Manager:

Terminal window
Install-Package Scrutor

Step 2: Add a using directive

In your Program.cs or wherever you configure services:

using Scrutor;

That’s it. No extra configuration or setup needed. You can now start scanning assemblies and auto-registering services with fluent, readable syntax.

Scrutor is lightweight, production-ready, and fully compatible with ASP.NET Core and Minimal APIs.

Basic Usage: Scan and Register by Convention

Scrutor shines when you want to register services based on a consistent naming or structure pattern—without explicitly wiring each one.

Let’s say you have several services following the pattern IServiceService, like:

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

Instead of registering each one manually, you can scan and register them all:

builder.Services.Scan(scan => scan
.FromAssemblyOf<IUserService>() // or any known type in the same assembly
.AddClasses(classes => classes.Where(type => type.Name.EndsWith("Service")))
.AsImplementedInterfaces()
.WithScopedLifetime());

What this does:

  • FromAssemblyOf<T>: Scans the assembly containing the specified type.
  • AddClasses(...): Filters classes to include (in this case, anything ending in Service).
  • AsImplementedInterfaces(): Maps each class to its interface(s).
  • WithScopedLifetime(): Sets the DI lifetime to Scoped (you can also use Transient or Singleton).

This approach drastically reduces boilerplate and ensures consistency across your registrations. As long as your services follow the conventions, Scrutor does the rest.

Now when you add a new service, just define the interface and class—no DI changes required.

Controlling Lifetimes: Scoped, Singleton, Transient

Scrutor gives you full control over how services are registered—just like the built-in DI container. You can choose between Scoped, Transient, and Singleton lifetimes during scanning.

You can learn more about Service Lifetimes in ASP.NET Core DI Container from this article..

Scoped (default in most APIs)

Creates one instance per request.

builder.Services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());

Transient

Creates a new instance every time it’s injected.

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

Singleton

Creates a single instance for the application’s lifetime.

builder.Services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses()
.AsImplementedInterfaces()
.WithSingletonLifetime());

You can also chain different lifetimes based on specific filters:

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

This gives you fine-grained control while keeping your DI setup clean and convention-based.

Filtering Registrations with Custom Rules

Scrutor isn’t just about scanning everything—it lets you filter exactly what gets registered, giving you precision and flexibility.

Filter by Namespace

Register only services from a specific namespace:

builder.Services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses(c => c.InNamespace("MyApp.Application.Services"))
.AsImplementedInterfaces()
.WithScopedLifetime());

Filter by Naming Convention

Only register classes that end with Service:

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

Exclude Specific Types

Skip certain classes from being registered:

builder.Services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses(c => c.Where(t => t.Name.EndsWith("Service") && t != typeof(EmailService)))
.AsImplementedInterfaces()
.WithScopedLifetime());

Filter by Interface

Only register classes that implement a specific base interface:

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

These filters help you avoid over-registration, reduce startup overhead, and enforce clean module boundaries. It’s especially useful in large projects where you only want to register infrastructure, domain, or application-layer services selectively.

Using Attributes for Fine-Grained Control

Scrutor also supports attribute-based registration, giving you more precise control over which services should be picked up during scanning. This is useful when you don’t want to rely solely on naming conventions or namespaces.

Step 1: Create a custom marker attribute

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

Step 2: Apply the attribute to your services

[Injectable]
public class AuditService : IAuditService
{
// implementation
}

Step 3: Scan and register only those classes

builder.Services.Scan(scan => scan
.FromAssemblyOf<IAuditService>()
.AddClasses(c => c.WithAttribute<InjectableAttribute>())
.AsImplementedInterfaces()
.WithScopedLifetime());

This way, only services explicitly marked with [Injectable] get registered—ignoring others, even if they match by name or interface.

Why use attribute-based control?

  • Prevents accidental registration of utility/helper classes
  • Keeps scanning logic declarative and self-documenting
  • Enables granular control in large shared libraries or modular apps

You can also extend this idea with different attributes for different lifetimes (e.g., [Singleton], [Scoped], [Transient])—though Scrutor doesn’t do that out of the box, you can easily implement it with custom logic if needed.

Scrutor with Clean Architecture

Scrutor fits perfectly into Clean Architecture, where your solution is broken into layers like:

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

Each layer contains its own set of services, and manually registering all of them becomes repetitive and error-prone as your app grows.

With Scrutor, you can scan each layer’s assembly and auto-register services based on conventions—keeping your Program.cs clean and your DI setup modular.

Application Layer Example

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

Infrastructure Layer Example

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

Benefits in Clean Architecture

  • No leakage between layers – scan only the assemblies you want
  • Scalable – easily register dozens of services across layers
  • DRY – reduce repetitive code and missed registrations
  • Testable – use constructor injection cleanly without worrying about service wiring

Each layer remains loosely coupled and self-contained, while Scrutor takes care of stitching them together at runtime. This keeps your architecture clean, flexible, and easy to scale.

Best Practices for Using Scrutor in Production

Scrutor is powerful—but like any automation, it needs structure. Here’s how to use it safely and effectively in real-world apps.

1. Be explicit with filters

Don’t scan everything. Use InNamespace, AssignableTo, or naming conventions to narrow down what gets registered.

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

2. Keep scanning scoped per layer

Scan only the assemblies related to each layer (Application, Infrastructure, etc.). This avoids accidental registrations and keeps boundaries clear.

.FromAssemblyOf<ISomeApplicationService>()

3. Avoid duplicate registrations

Scrutor doesn’t warn you about multiple services for the same interface unless configured. Use .TryAdd*() from Microsoft.Extensions.DependencyInjection if needed, or make sure your filters are tight.

4. Separate scanning logic per concern

Instead of one huge Scan block, break registrations into sections per layer/module.

RegisterApplicationServices();
RegisterInfrastructureServices();

5. Use attributes sparingly

Only when you need fine-grained control. Don’t overdo it—conventions are easier to manage long-term.

6. Document the conventions

Make it clear in your project README or architecture docs how services are discovered. This helps onboard new devs and avoids hidden magic.

7. Don’t scan external/third-party assemblies

Only scan your own code. External libs may include types that accidentally match your filters.

8. Profile startup performance if you’re scanning large assemblies

For massive projects, scanning can add startup time. Cache compiled expressions or benchmark registration if needed.

Scrutor makes DI cleaner—but the structure is still your responsibility. Keep it tight, intentional, and aligned with your architecture.

Conclusion

Scrutor helps you stop fighting with boilerplate and start focusing on actual app logic. By auto-registering services through conventions and scanning, it brings clarity, consistency, and scalability to your Dependency Injection setup—especially in layered architectures like Clean Architecture.

Whether you’re working on a small service or a large enterprise app, Scrutor simplifies your DI configuration without compromising flexibility. Just define your conventions, apply smart filters, and let the container handle the rest.

Keep your registrations clean, predictable, and maintainable—with Scrutor, DI becomes one less thing to worry about.


🚀 Want to learn more about modern .NET practices like this?
Check out my FREE .NET Web API Zero to Hero course:
https://codewithmukesh.com/courses/dotnet-webapi-zero-to-hero/

It covers everything from DI to Clean Architecture, Minimal APIs, and production-ready patterns.

Support ❤️
If you have enjoyed my content, support me by buying a couple of coffees.
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Level Up Your .NET Skills

Join my community of 8,000+ developers and architects.
Each week you will get 1 practical tip with best practices and real-world examples.