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!
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!
Affiliations
These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!
- BrandGhost: My social media content and scheduling tool that I use for ALL of my content!
- RackNerd: Cheap VPS hosting options that I love for low-resource usage!
- Contabo: Affordable VPS hosting options!
- ConvertKit: The platform I use for my newsletter!
- SparkLoop: Helps add value to my newsletter!
- Opus Clip: Tool for creating short-form videos!
- Newegg: For all sorts of computer components!
- Bulk Supplements: Huge selection of health supplements!
- Quora: I answer questions when folks request them!