Improving ASP.NET Core Dependency Injection With Scrutor

Improving ASP.NET Core Dependency Injection With Scrutor

4 min read ·

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 scan
  • AddClasses - adds the classes from the selected assemblies
  • UsingRegistrationStrategy - defines which RegistrationStrategy to use
  • AsMatchingInterface - registers the types as matching interfaces (ClassNameIClassName)
  • 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 exists
  • RegistrationStrategy.Append- appends a new registration for existing services
  • RegistrationStrategy.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!


Whenever you're ready, there are 4 ways I can help you:

  1. (COMING SOON) REST APIs in ASP.NET Core: You will learn how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API. Join the waitlist!
  2. Pragmatic Clean Architecture: Join 3,150+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 1,050+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
  5. Promote yourself to 58,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 58,000+ engineers who are improving their skills every Saturday morning.