Automate Your .NET DI Setup with Scrutor (Step-by-Step Guide)
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 assemblyAddClasses(...)→ Selects classes ending with ServiceAsImplementedInterfaces()→ Maps them to their interfacesWithScopedLifetime()→ 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
- Be explicit with filters – Don’t scan everything blindly.
- Scan per layer – Keep boundaries clear (Application, Infrastructure, etc.).
- Avoid duplicates – Tighten filters or use
TryAdd*()if needed. - Organize registrations – Group scans by concern.
- Use attributes only when necessary – Keep conventions as your main driver.
- Document your conventions – Helps new developers understand your setup.
- Don’t scan external assemblies – Focus on your codebase only.
- 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. 💪
