Scrutor in C# - 3 Simple Tips to Level Up Dependency Injection

Dependency Injection in C# is a lifesaver when organizing dependencies -- especially in more complex ASP.NET Core applications. And if you're already familiar with IServiceCollection or just want to stick as close as possible to the already provided DI offerings, Scrutor in C# is a great enhancement.

In this article, I'll provide you with a high-level overview of Dependency Injection and Scrutor in C#. But from there, we're jumping right into 3 tips that you can use with Scrutor that could prove to be very helpful in your application!


What's In This Article: Scrutor in C#

Remember to check out these platforms:

// FIXME: social media icons coming back soon!


What is Scrutor in C#?

Scrutor is a powerful NuGet package in C# that enhances dependency injection. It simplifies the registration and resolution of dependencies, making it easier for you to manage and organize your code.

Dependency injection is a popular approach to configuring applications that promotes loose coupling and improves testability and maintainability. It involves injecting dependencies into a class rather than directly creating them within the class. This means that the responsibility of creating the dependency is not owned by the class requiring it, but instead, owned by someone up the call stack. Eventually, this pushes nearly all dependency creation to the entry point of an application, making it unwieldy. But dependency injection frameworks help clean up and organize all of this logic.

Scrutor takes this concept a step further by providing a simple and intuitive way to register and resolve dependencies. With Scrutor, you no longer need to manually register each dependency one by one. Instead, you can use conventions and attributes to automate the process.

1 - Using Scrutor for Registration and Resolution

To illustrate how to use Scrutor in C# for registration and resolution, let's consider a simple scenario. Imagine we have an application that requires the use of different data repositories, such as UserRepository and ProductRepostiory.

First, we need to install the Scrutor NuGet package in our project. Open the Package Manager Console and run the following command:

Install-Package Scrutor 

Next, we need to define our repositories and their corresponding interfaces. This example is essentially empty, but we'll be referring to these in a moment:

public interface IUserRepository
{
    // Interface methods
}
public class UserRepository : IUserRepository 
{
    // Implementation
}

public interface IProductRepository
{
    // Interface methods
}

public class ProductRepository : IProductRepository
{
    // Implementation
}

Now, let's wire up our dependencies using Scrutor. In your startup code (such as in the ConfigureServices method in ASP.NET Core), add the following code:

services.Scan(scan => scan
    .FromAssemblyOf<Startup>()
    .AddClasses(classes => classes.AssignableToAny(
        typeof(IUserRepository),
        typeof(IProductRepository)))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

This code uses the Scan method from Scrutor to scan the assembly that contains the Startup class. It then filters out the classes that are assignable to IUserRepository or IProductRepository interfaces. Finally, it maps the implementations of these interfaces and registers them with their respective interfaces.

Now, we can inject these dependencies into our classes. For example, let's say we have a UserService class that requires IUserRepository:

public class UserService 
{
    private readonly IUserRepository _userRepository; 
    
    public UserService(IUserRepository userRepository) 
    { 
        _userRepository = userRepository;
    }

    // Rest of the class implementation
}

By declaring IUserRepository as a dependency in the constructor, Scrutor along with IServiceCollection will automatically resolve and inject the UserRepository instance for us.


2 - The Decorator Pattern - Example of Logging for Services

Imagine you have a service interface IService and an implementation MyService. You want to log the entry and exit of each method call in MyService without polluting its business logic with logging concerns.

First, define the IService interface and its implementation:

public interface IService
{
    void DoWork();
}

public class MyService : IService
{
    public void DoWork()
    {
        // Business logic here
    }
}

Next, create a decorator class LoggingDecorator that implements IService. This class will wrap the actual service and add logging around the delegated method calls:

public class LoggingDecorator : IService
{
    private readonly IService _decorated;
    private readonly ILogger<LoggingDecorator> _logger;

    public LoggingDecorator(IService decorated, ILogger<LoggingDecorator> logger)
    {
        _decorated = decorated;
        _logger = logger;
    }

    public void DoWork()
    {
        _logger.LogInformation("Starting work.");
        _decorated.DoWork();
        _logger.LogInformation("Finished work.");
    }
}

Now, use Scrutor to register your service and its decorator in the Startup.cs or wherever you configure your services:

public void ConfigureServices(IServiceCollection services)
{
    // Register the base service
    services.AddScoped<IService, MyService>();

    // Use Scrutor to apply the decorator
    services.Decorate<IService, LoggingDecorator>();

    // Make sure to register the ILogger or ILogger<T> dependencies if not already done

}

This setup uses Scrutor to wrap the IService registration with the LoggingDecorator. When resolving IService, the DI container will provide an instance of LoggingDecorator, which in turn wraps an instance of MyService. This approach achieves the separation of concerns by keeping the logging logic outside of your business logic.


3 - Feature Toggle for Service Implementations

When we're building complex systems, there are often cases where we want to introduce something like feature flags. These allow us to take different code paths based on configuration and not have to recompile and deploy an entire application. In some situations, it's not just about picking a different code path, it's about swapping an entire implementation of something!

Let's try out an example together! Assume you have an IFeatureService interface with multiple implementations: NewFeatureService (a new, experimental feature) and StandardFeatureService (the current, stable feature). You want to switch between these implementations at runtime based on a configuration flag.

First, define the IFeatureService interface and its implementations:

public interface IFeatureService
{
    void ExecuteFeature();
}

public class NewFeatureService : IFeatureService
{
    public void ExecuteFeature()
    {
        // New feature logic
    }
}

public class StandardFeatureService : IFeatureService
{
    public void ExecuteFeature()
    {
        // Standard feature logic
    }
}

Next, you need a way to determine which implementation to use based on a feature toggle setting. This could be a value in appsettings.json, a database setting, or any other configuration source.

Now, use Scrutor to dynamically register the appropriate service implementation based on the feature toggle:

public void ConfigureServices(IServiceCollection services)
{
    // Assume GetFeatureToggleValue() retrieves the current feature toggle setting
    var useNewFeature = GetFeatureToggleValue("UseNewFeature");

    // Dynamically register the appropriate implementation based on the feature toggle
    if (useNewFeature)
    {
        services.AddScoped<IFeatureService, NewFeatureService>();
    }
    else
    {
        services.AddScoped<IFeatureService, StandardFeatureService>();
    }

    // Optional: You could combine this with the previous example
    // to use the decorator pattern too!
    // services.Decorate<IFeatureService, FeatureServiceProxy>();
}

private bool GetFeatureToggleValue(string featureName)
{
    // This method would typically check your
    // configuration source to determine if the feature is enabled
    // For simplicity, this is just a placeholder
    return false; // or true based on actual configuration
}

Taking an approach like this can help us with:

  • Flexibility: Allows the application to switch between feature implementations without needing to redeploy. You can enable or disable features with just a configuration change.
  • Testing in Production: You can test new features in a production environment with a limited set of users by toggling the feature on for those users.
  • Gradual Rollouts: Helps in rolling out features gradually to monitor the impact and ensure stability before a wider release.
  • Risk Mitigation: Quickly revert to the old implementation if issues arise with the new feature, minimizing downtime and impact on users.

Wrapping Up Scrutor in C#

In this article, I provided you with 3 simple tips for dependency injection with Scrutor in C#. After a brief overview of what dependency injection is and how Scrutor fits into it, we jumped right into our examples. We got to see assembly scanning, how to decorate dependencies, and even how to consider feature flagging entire implementations!

Dependency injection is something that can greatly simplify your applications as they grow in complexity, and Scrutor can be a huge help if you want to stick to the built-in IServiceCollection offering from Microsoft. If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Join me and the other community members on Discord!