Dependency Injection (DI) is a foundational design pattern in modern .NET applications. It promotes loosely coupled code, improves testability, and makes it easier to manage complex dependencies.
However, as your project grows, managing DI registrations manually can become tedious and error-prone. Copy-pasting services.AddTransient<>();
lines across multiple files isn’t just boring—it’s a maintainability nightmare waiting to happen.
That’s where Scrutor comes in.
Scrutor is a lightweight .NET library that extends the built-in Microsoft Dependency Injection container with powerful features like assembly scanning, convention-based registration, and decorators—all without introducing a new DI framework. With Scrutor, you can clean up your Startup.cs or Program.cs and let your services register themselves intelligently.
In this post, we’ll explore what Scrutor is, why you might want to use it, and how to integrate it into your application step-by-step.
Scrutor is an open-source extension library for the built-in Dependency Injection (DI) container in .NET. Rather than replacing the Microsoft.Extensions.DependencyInjection system, it enhances it with powerful capabilities like assembly scanning, convention-based registration, and service decoration.
At its core, Scrutor allows you to automatically discover and register services based on patterns and conventions, dramatically reducing the need for manual services.AddTransient() or services.AddScoped() declarations. This leads to cleaner startup code and makes your application easier to scale and maintain.
Here’s what makes Scrutor valuable:
Manually registering services works fine for small applications—but once your project grows, it quickly turns into a repetitive chore. You end up scattering AddScoped
, AddTransient
, and AddSingleton
calls across multiple files, often duplicating logic or missing bindings altogether.
Scrutor helps you eliminate this manual overhead through convention-based registration and smart assembly scanning. Here’s why it’s worth using:
Scrutor cuts down on repetitive code. Instead of registering each service manually, you can register entire groups of services based on interface conventions or naming patterns.
services.Scan(scan => scan
.FromAssemblyOf(Assembly.GetExecutingAssembly())
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());
As your project adds more services, Scrutor scales with you. No need to update your DI setup every time you create a new class—just follow your established conventions, and Scrutor will handle the registration.
Scrutor makes it easy to wrap services with decorators for cross-cutting concerns like logging, retry logic, or caching—without rewriting your core service logic.
Scrutor is available as a NuGet package and is compatible with the built-in Microsoft.Extensions.DependencyInjection
container used in ASP.NET Core and other modern .NET applications.
You can add Scrutor to your project using the .NET CLI:
dotnet add package Scrutor
If you’re using the Package Manager Console in Visual Studio:
Install-Package Scrutor
Scrutor gives you powerful primitives to make dependency injection smarter and cleaner. Let’s walk through the most useful features with examples.
The most popular feature of Scrutor is assembly scanning. It allows you to automatically discover and register services without declaring each one manually.
services.Scan(scan => scan
.FromAssemblyOf<IMyService>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime());
This snippet finds all non-abstract, non-generic classes in the assembly containing IMyService
, and registers them as their implemented interfaces using the scoped lifetime.
You can narrow the scan by interface, namespace, base class, or naming convention:
services.Scan(scan => scan
.FromAssemblyOf<IMyService>()
.AddClasses(classes => classes.AssignableTo<IMyService>())
.AsImplementedInterfaces());
Or filter by namespace:
.AddClasses(c => c.InNamespaceOf<MyNamespaceMarker>())
You can also register classes as themselves with .AsSelf()
, or as both with .AsSelfWithInterfaces()
.
Scrutor lets you include or exclude types based on attributes. For example, you can only register types marked with [Injectable]
:
.AddClasses(c => c.WithAttribute<InjectableAttribute>())
Or exclude types:
.AddClasses(c => !c.Name.EndsWith("Mock"))
Scrutor supports the standard lifetimes (Transient
, Scoped
, Singleton
). Just chain .WithScopedLifetime()
or the others at the end of your fluent call.
.WithTransientLifetime()
.WithScopedLifetime()
.WithSingletonLifetime()
Scrutor also supports service decoration. This is perfect for wrapping cross-cutting concerns around existing implementations.
services.TryDecorate<IService, LoggingServiceDecorator>();
You can chain multiple decorators too:
services.TryDecorate<IService, RetryDecorator>();
services.TryDecorate<IService, LoggingServiceDecorator>();
Scrutor will apply them in order of registration.
Let’s bring Scrutor into some practical scenarios you’re likely to encounter in real .NET projects.
Imagine you have a folder like Services/
with multiple implementations of business logic:
public interface IOrderService { }
public class OrderService : IOrderService { }
public interface ICustomerService { }
public class CustomerService : ICustomerService { }
You can automatically register them using:
services.Scan(scan => scan
.FromAssemblyOf<IOrderService>()
.AddClasses(c => c.InNamespaceOf<IOrderService>())
.AsImplementedInterfaces()
.WithScopedLifetime());
No need to manually register every new service—Scrutor handles it.
Suppose you want to wrap all services implementing IOrderService
with a logging decorator:
public class LoggingOrderServiceDecorator : IOrderService
{
private readonly IOrderService _inner;
private readonly ILogger<LoggingOrderServiceDecorator> _logger;
public LoggingOrderServiceDecorator(IOrderService inner, ILogger<LoggingOrderServiceDecorator> logger)
{
_inner = inner;
_logger = logger;
}
public void PlaceOrder(Order order)
{
_logger.LogInformation("Placing order");
_inner.PlaceOrder(order);
}
}
Register the original implementation and apply the decorator:
services.AddScoped<IOrderService, OrderService>();
services.TryDecorate<IOrderService, LoggingOrderServiceDecorator>();
Scrutor offers a lot of flexibility, but like any powerful tool, it’s best used thoughtfully. Here are some tips and best practices to get the most out of it:
Scrutor works best when your codebase follows consistent patterns. For example:
OrderService
implementing IOrderService
These conventions make it easier to scan and register components with minimal code as well as improve readability for new developers.
Instead of putting all services.Scan(...)
calls in Program.cs
, extract them into reusable extension methods:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
services.Scan(scan => scan
.FromAssemblyOf<IOrderService>()
.AddClasses(c => c.InNamespaceOf<IOrderService>())
.AsImplementedInterfaces()
.WithScopedLifetime());
return services;
}
}
Then call it cleanly in your startup logic:
services.AddBusinessServices();
It’s tempting to scan entire assemblies with .AddClasses()
, but it can lead to unexpected registrations. Instead, filter by:
This keeps your DI container predictable and avoids conflicts or hidden bugs.
Decorators are powerful, but chaining too many can introduce performance issues or make behavior harder to trace. Use them intentionally for clear cross-cutting concerns (e.g. logging, retries, validation).
While Scrutor is powerful and easy to adopt, it’s important to understand where it might not be the best fit—or where caution is warranted.
If you’re not careful with your scan filters, you might register more services than you intended, especially when using broad AddClasses()
scans. This can lead to:
Be specific with your filters (e.g. by namespace, base class, or marker interface) to avoid surprises.
Convention-based registration can make your DI setup less explicit. Developers unfamiliar with Scrutor might have a hard time tracing where a service is registered or why it’s resolving in a particular way.
Documentation, naming conventions, and strategic use of .AsSelf()
or .AsImplementedInterfaces()
help keep things understandable.
Assembly scanning can have a slight performance cost during startup—especially if you’re scanning very large assemblies without filtering. While usually negligible, it’s worth profiling if your app has a large plugin/module system.
In unit or integration tests, Scrutor-based registration may complicate test isolation if too many services are automatically wired up. Consider using explicit registration in test projects for better control.
Scrutor doesn’t offer built-in support for conditional service registration (e.g. based on environment or configuration). If you need more advanced behaviors, you may need to fall back to regular if/else
logic or a more full-featured container like Autofac.
While Scrutor offers a lightweight and elegant solution for most .NET applications, it’s not the only option available. Here’s how it stacks up against some alternatives:
Pros:
Cons:
Scrutor shines here by reducing repetition while still keeping things predictable when used with filters.
Autofac is a powerful, feature-rich container that supports:
If you need fine-grained control and advanced features, Autofac might be a better fit. However, it comes with additional complexity and setup.
These offer different philosophies and capabilities but require fully replacing the default container, which adds friction—especially for ASP.NET Core projects.
Scrutor works with the built-in container, making it an ideal choice when you want better registration without adopting a whole new framework.
Scrutor is best when:
If you need features like conditional resolution or child containers, consider using a more advanced DI framework. Otherwise, Scrutor offers an ideal middle ground.
Scrutor is one of those libraries that quietly upgrades your development experience. It doesn’t try to replace the default .NET DI container—it enhances it with features you wish were built-in: assembly scanning, convention-based registration, and decorators.
Whether you’re building a microservice, a modular monolith, or just tired of repetitive service registration, Scrutor can help you write cleaner, more maintainable code.
By following best practices, understanding its limitations, and using its features intentionally, you can make DI in your .NET applications both powerful and elegant.
If you haven’t tried Scrutor yet, now’s the perfect time to clean up your Program.cs
and let your services register themselves. Until next time, stay curious! 🚀