Thank you to our sponsors who keep this newsletter free to the reader:
Progress prepared a free eBook for you - Planning a Blazor Application. This eBook documents a high-level outline of what developers need to consider when choosing Blazor: migration strategies, target platforms, tooling, testing and UI. Get the Blazor eBook here.
IcePanel is a collaborative C4 model modelling & diagramming tool that helps explain complex software systems. With an interactive map, you can align your software engineering & product teams on technical decisions across the business. Check it out here.
Dependency injection (DI) is one of the most exciting features of ASP.NET Core. It helps us build more testable and maintainable applications. However, ASP.NET Core's built-in DI system sometimes needs a little help to achieve more advanced scenarios.
So I want to introduce you to a powerful library for enhancing your ASP.NET Core DI - Scrutor.
If you're an ASP.NET Core developer, you're already familiar with Dependency Injection. It's a fundamental part of building modular and maintainable applications.
Let's explore how Scrutor can simplify and enhance your DI setup.
What is Dependency Injection?
Dependency Injection is a software design pattern used in ASP.NET Core to achieve the Inversion of Control (IOC) principle. This promotes loose coupling and makes your code more testable, maintainable, and extensible.
DI allows you to inject dependencies into your classes rather than create them within the class. The framework takes care of providing the required instances at runtime. It also manages the disposal of these dependencies based on the service lifetime.
Here's an example of combining constructor and method injection in a controller:
[ApiController]
[Route("api/activities")]
public class ActivitiesController : ControllerBase
{
private readonly ILogger<ActivitiesController> _logger;
// Constructor injection
public ActivitiesController(ILogger<ActivitiesController> logger)
{
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> Get(ISender sender) // Method injection
{
var activities = await sender.Send(new GetActivitiesQuery());
return Ok(activities);
}
}
Service Lifetimes in ASP.NET Core
Before we dive into Scrutor, let's briefly discuss service lifetimes in ASP.NET Core. When you register a service in the DI container, you specify its lifetime. The service lifetime defines how long the DI container should maintain the service.
ASP.NET Core provides three main lifetimes:
- Singleton: A single instance of the service is created and reused throughout the application's lifetime.
- Scoped: A new instance is created for each scope (usually a web request). Services created in the same scope share the same instance.
- Transient: A new instance is created every time the service is requested.
Understanding service lifetimes is crucial when designing your application's architecture.
What is Scrutor?
The Scrutor library improves your dependency injection code by extending the existing features from Microsoft.Extensions.DependencyInjection
.
These extensions add support for advanced assembly scanning and service decoration.
To get started using Scrutor, you need to install the NuGet package:
Install-Package Scrutor
Assembly Scanning With Scrutor
One of the most powerful features of Scrutor is its ability to perform assembly scanning. Rather than manually registering each service, Scrutor allows you to scan your assemblies for types that should be registered with the DI container. This can significantly reduce the boilerplate code required for service registration, making your code cleaner and more maintainable.
The entry point for assembly scanning is the Scan
method, which accepts a delegate to define the DI setup.
Here's an example of scanning two assemblies and registering the classes inside as scoped services:
builder.Services.Scan(selector => selector
.FromAssemblies(
typeof(PersistenceAssembly).Assembly,
typeof(InfrastructureAssembly).Assembly)
.AddClasses(publicOnly: false)
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
.AsMatchingInterface()
.WithScopedLifetime());
Let's unpack what's happening here:
FromAssemblies
- allows you to specify which assemblies to scanAddClasses
- adds the classes from the selected assembliesUsingRegistrationStrategy
- defines whichRegistrationStrategy
to useAsMatchingInterface
- registers the types as matching interfaces (ClassName
→IClassName
)WithScopedLifetime
- registers the types with a scoped service lifetime
There are three values for RegistrationStrategy
you can use:
RegistrationStrategy.Skip
- skips registrations if service already existsRegistrationStrategy.Append
- appends a new registration for existing servicesRegistrationStrategy.Throw
- throws when trying to register an existing service
You can also specify a filter to AddClasses
to select specific types you want to configure.
Here's an example of registering repository implementations:
services.Scan(scan => scan
.FromAssemblies(typeof(PersistenceAssembly).Assembly)
.AddClasses(
filter => filter.Where(x => x.Name.EndsWith("Repository")),
publicOnly: false)
.UsingRegistrationStrategy(RegistrationStrategy.Throw)
.AsMatchingInterface()
.WithScopedLifetime());
Service Decoration With Scrutor
Service decoration is another valuable feature offered by Scrutor. It enables you to modify or extend services during registration without changing the original implementation.
This is incredibly useful when adding cross-cutting concerns or other modifications to services without altering their core functionality. For example, you can implement a caching decorator for repositories.
Here's how you can configure a decorator with Scrutor's Decorate
method:
services.AddScoped<IActivitiesRepository, ActivitiesRepository>();
services.Decorate<IActivitiesRepository, PermissionActivitiesRepository>();
It will decorate the ActivitiesRepository
service using the PermissionActivitiesRepository
.
This also means that PermissionActivitiesRepository
can inject an IActivitiesRepository
instance, and at runtime, this is resolved as ActivitiesRepository
.
Here's how you can implement the PermissionActivitiesRepository
:
public class PermissionActivitiesRepository : IActivitiesRepository
{
private readonly IActivitiesRepository _decorated;
private readonly IPermissionChecker _permissionChecker;
public PermissionActivitiesRepository(
IActivitiesRepository decorated,
IPermissionChecker permissionChecker)
{
_decorated = decorated;
_permissionChecker = permissionChecker;
}
public List<Activity> Get()
{
if (!_permissionChecker.HasPermission(Permissions.FetchActivities))
{
return new();
}
return _decorated.Get();
}
}
Takeaway
Scrutor can improve your ASP.NET Core DI by simplifying service registration through assembly scanning and enabling service decoration. You can use Scrutor's capabilities to write cleaner, more maintainable, and flexible DI code while reducing the complexity of your startup configuration.
Assembly scanning can reduce the boilerplate code required for service registration. It also allows you to create custom conventions for registering services.
Service decoration has been a real game-changer for me. It's the simplest way to introduce cross-cutting concerns in your application. For example, I used to add an idempotency check before handling events.
Hope this was valuable.
Stay awesome!