In this article, we will explore the Scrutor library, learn about its use cases, what it offers to simplify dependency injection, and the implementation of cross-cutting concerns using the decorator pattern.
Let’s start.
What Is Scrutor?
The Scrutor package improves our dependency injection code by providing a set of extensions to the .NET built-in dependency container. These extensions add support for out-of-the-box assembly scanning and type decoration.
Using Scrutor for Assembly Scanning
Scrutor helps to simplify our dependency injection code by dynamically searching types inside assemblies and registering them at runtime. Leveraging assembly scanning we can partially automate our dependency registration code.
Additionally, we can use assembly scanning to build extensible systems. With scanning, we can build a program able to find and load additional modules at run time.
Automating Dependency Registration
In our ASP.NET web API project, let’s define a User
entity:Â
public class User { public required int Id { get; init; } public required string FirstName { get; init; } public required string LastName { get; init; } }
After that, let’s create a service interface to manage the User
entity:
public interface IUserService { User GetUser(int id); }
In the IntroductionToScrutorInDotNet.Services.Implementations
namespace, let’s define an implementation for the IUserService
:
public class UserService : IUserService { public User GetUser(int id) { return new User { Id = id, FirstName = "John", LastName = "Doe" }; } }
Finally, let’s create a UsersController
to allow us to query the user using an HTTP Get request:Â
[Route("[controller]")] [ApiController] public class UsersController : ControllerBase { private readonly IUserService _userService; public UsersController(IUserService userService) { _userService = userService; } [HttpGet("{id:int}")] public IActionResult GetUser(int id) { return Ok(_userService.GetUser(id)); } }
Now, we need to wire the UsersController
with the UserService
, we can do that using our dependency injection framework.
Instead of doing a manual registration of the UserService
, we can use Scrutor’s Scan()
extension method, to register all the services automatically:
builder.Services.Scan(selector => selector .FromCallingAssembly() .AddClasses( classSelector => classSelector.InNamespaces("IntroductionToScrutorInDotNet.Services.Implementations") ) .AsImplementedInterfaces() );
Here, we call Scrutor’s Scan()
extension method providing a selector action as an argument. The method will perform an assembly scanning and will select services to register based on the selector we provide.Â
First of all, we need to select the assembly where our target types reside, here we have chosen to get the types FromCallingAssembly()
. Next, we narrow the selection even further, to only register classes by calling the AddClasses()
method.
The AddClasses()
method also has a selector as a parameter, we use that to only select classes in the IntroductionToScrutorInDotNet.Services.Implementations
namespace.
Finally, we register our selected types as a substitution for all the interfaces they implement by calling AsImplementedInterfaces()
. This is because our UsersController
depends on the IUserService
and not directly on the UserService
.
Using the wide variety of extension methods that Scrutor provides we can be very precise about the services we want to register.
Registering Services in Another Assembly
Let’s consider another .NET class library project IntroductionToScrutorInDotNet.Customers
that contains types to manage the Customer
entity:Â
- The definition of the
Customer
Entity - The interface
ICustomerService
- The implementation
CustomerService
for theICustomerService
interface
Our CustomersController
needs to use the ICustomerService
to create and return a customer for a certain User
:
[Route("[controller]")] [ApiController] public class CustomersController : ControllerBase { private readonly ICustomerService _customerService; public CustomersController(ICustomerService customerService) { _customerService = customerService; } [HttpPost] public IActionResult CreateCustomer(User user) { var fullName = string.Join(' ', user.FirstName, user.LastName); var customerId = Random.Shared.Next(1000); return Created( $"/Customers/{customerId}", _customerService.CreateCustomer(customerId, fullName) ); } }
With Scrutor, we can register the types from the IntroductionToScrutorInDotNet.Customers
project assembly:
builder.Services.Scan(selector => selector .FromAssemblyOf<ICustomerService>() .AddClasses(classSelector => classSelector.AssignableTo<ICustomerService>()) .AsMatchingInterface());
Here, we are asking Scrutor to scan only the assembly in which ICustomerService
 interface resides using the FromAssemblyOf<ICustomerService>()
extension method. Then, among all the implementations in that assembly, we select only those that are assignable to ICustomerService.
Finally, we instruct Scrutor to register the selected implementations to the interfaces that match the implementation class name by calling the AsMatchingInterface()
method.
Working With Generics
Now, we want to add a repository to get the list of Users
. First, we define the generic repository interface:Â
public interface IRepository<T> { IEnumerable<T> Get(); }
Then, we implement the repository to use a list as a back storage for our User
entity:
public class UserRepository : IRepository<User> { private readonly List<User> _users = new() { new User { Id = 1, FirstName = "John", LastName = "Doe" }, new User { Id = 2, FirstName = "Janine", LastName = "Doe" } }; public IEnumerable<User> GetAll() { return _users; } }
Let’s update our UsersController
to use the repository to get all the users we have in our system:
public class UsersController : ControllerBase { private readonly IUserService _userService; private readonly IRepository<User> _userRepository; public UsersController(IUserService userService, IRepository<User> userRepository) { _userService = userService; _userRepository = userRepository; } [HttpGet] public IActionResult GetUsers() { return Ok(_userRepository.GetAll()); } }
Last, we tell Scrutor to register all implementations of the open generic type IRepository<>
as their implemented interfaces:Â
builder.Services.Scan(selector => selector .FromCallingAssembly() .AddClasses(classSelector => classSelector.AssignableTo(typeof(IRepository<>))) .AsImplementedInterfaces());
This will register our UserRepository
to substitute any dependency on IRepository<User>
.
Specifying the Lifetime of Dependencies
As with any dependency management framework, specifying the lifetime of the instances created by the container is an essential part of dependency registration. With Scrutor, we can use lifetime specifiers:
builder.Services.Scan(selector => selector .FromAssemblyOf<ICustomerService>() .AddClasses(classSelector => classSelector.AssignableTo<ICustomerService>()) .AsMatchingInterface() .WithTransientLifetime() );
In this example, we are registering all the matching implementations with a transient lifetime scope by calling WithTransientLifetime()
. There are similar extension methods corresponding to the common .NET lifetime scopes, for example, WithSingletonLifetime()
or WithScopedtLifetime()
.
Handling Multiple Implementations
Now let’s see how we can set our desired registration strategy for each selector. Registration strategy is how Scrutor handles the cases where multiple implementations exist for the same interface:
builder.Services.Scan(scan => scan.FromAssemblyOf<ICustomerService>() .AddClasses(classes => classes.InExactNamespaceOf<ICustomerService>()) .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() );
In this code, by using the UsingRegistrationStrategy()
method with the RegistrationStrategy.Skip
parameter, we are telling Scrutor to ignore additional implementations of any interface once the first one has been registered.
Other registration strategies available include:
RegistrationStrategy.Append
: Appends a new registration for existing servicesRegistrationStrategy.Throw
: Throws when trying to register an existing service
Chaining Multiple Registrations
 For now, we have been configuring Scrutor for each part of our application separately. It’s possible for the sake of simplicity and clarity to register everything with a single call to the Scan()
method.
Thanks to the chaining ability of different extension methods, we can replace all Scrutor configurations we have written with a single call:
builder.Services.Scan(selector => selector .FromCallingAssembly() .AddClasses( classSelector => classSelector.InNamespaces("IntroductionToScrutorInDotNet.Services.Implementations") ) .AsImplementedInterfaces() .AddClasses(classSelector => classSelector.AssignableTo(typeof(IRepository<>))) .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() .FromAssemblyOf<ICustomerService>() .AddClasses(classSelector => classSelector.AssignableTo<ICustomerService>()) .AsMatchingInterface() .WithTransientLifetime() );
Decorator Pattern With Scrutor
We can use the decorator design pattern to extend the functionality of an existing object without changing its code. For this purpose, a decorator is designed as a wrapper around the objects that need to be extended so it can intercept calls to the wrapped object’s methods.
Implementing a Simple Decorator
To demonstrate how we can leverage Scrutor’s Decorate()
extension method to manage the registration of decorator objects, let’s define a decorator of our repository that logs to the console when we access a list of entities.Â
To do this, we need to inherit from the same interface that our repository implements:
public class RepositoryLoggerDecorator<T> : IRepository<T> { private readonly IRepository<T> _decoratedRepository; public RepositoryLoggerDecorator(IRepository<T> decoratedRepository) { _decoratedRepository = decoratedRepository; } public IEnumerable<T> GetAll() { Console.WriteLine("The list of all users has been retrieved from the DB"); return _decoratedRepository.GetAll(); } }
The decorator should take in its constructor the instance of the decorated repository, in our case it’s the instance of IRepository<T>
that we will wrap. In the GetAll()
method we are logging a message to the console and then we are calling the _decoratedRepository
‘s GetAll()
method.
Registering Decorators With Scrutor
Let’s proceed to register our decorator with our dependency container using Scrutor’s Decorate<,>()
extension method:
builder.Services.Decorate<IRepository<User>, RepositoryLoggerDecorator<User>>();
The first type argument of the Decorate<,>()
method represents the type of service we want to decorate and the second type argument is the type of the decorator.
After we register the decorator with Scrutor, all calls to IRepository<User>
methods will be intercepted by the RepositoryLoggingDecorator<User>
. If we run the application and call the HTTP Get endpoint to get all users, we can see that the log message is written to the console.
In this example, we have used Console.WriteLine()
method to log, in a real-life scenario we should use ILogger<T> and a logging framework.
Conclusion
In this article, we have learned what is the Scrutor library and how to use it for assembly scanning. We have also learned what the decorator pattern is and how Scrutor can help us easily manage decorators in our dependency container.