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:
dotnet add package Scrutor
Or via Package Manager:
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 IService
→ Service
, 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 inService
).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.