DEV Community

DeviantCoding
DeviantCoding

Posted on • Originally published at Medium

Registerly: Simplifying Dependency Injection with Attributes

Dependency Injection (DI) is a fundamental design pattern in modern software development, particularly in .NET applications. It promotes loose coupling and enhances testability by allowing dependencies to be injected rather than hard-coded. However, managing DI registrations can become cumbersome as the number of services grows. This is where Registerly comes into play.

DeviantCoding.Registerly is a powerful NuGet package that simplifies the process of registering services in Microsoft dependency injection container using attributes. It started as an improvement on Scrutor’s handling of attributes for self-registration and has evolved into a complete replacement for most scenarios.

Registerly is, and will be, Open Source. You can download the source code and (hopefully) contribute to its development at DeviantCoding’s site on GitHub.

In this article, we will explore how to use Registerly to streamline your DI registrations with practical examples.

Getting Started

To get started with Registerly, you need to install the NuGet package. You can do this via the NuGet Package Manager in Visual Studio, by running the following command in the Package Manager Console:

Install-Package DeviantCoding.Registerly
Enter fullscreen mode Exit fullscreen mode

Or, using dotnet CLI:

dotnet add package DeviantCoding.Registerly
Enter fullscreen mode Exit fullscreen mode

Once installed, you can start using Registerly to register your services with attributes.

Basic Usage

When dealing with DI, 99% of times, your need is exactly the same: registering a class with its implemented interfaces.

Preparing our application

To achieve this with Registerly, we start by simply telling our HostBuilder that it needs to search for the classes to register, with a single line of code:

using Microsoft.Extensions.Hosting;

var builder = Host.CreateDefaultBuilder(args);
// Call the following method any time before building the application
builder.RegisterServicesByAttributes();

var app = builder.Build();
Enter fullscreen mode Exit fullscreen mode

Marking classes to register

Then, we have to mark the services we want to register with a life-time attribute (Singleton, Scoped, Transient), like this:

using DeviantCoding.Registerly.AttributeRegistration;

public interface IGreeter
{
  string Greet();
}

public interface IFareweller
{
  string Farewell();
}

[Scoped] // This attribute marks this class for DI Registration
public class ChatterboxService: IGreeter, IFareweller
{
  public string Greet() => "Hi there!";
  public string Farewell() => "See ya later, alligator!";
}
Enter fullscreen mode Exit fullscreen mode

And… That’s all it takes!

Next time you add IGreeter or IFareweller as a dependency of a class, DI will create an instance of ChatterboxService, and pass it to your class.

Advanced Usage

Custom Mapping Strategies

Under the hood, what we have seen in the previous section has been an example of the usage of an IMappingStrategy called AsImplementedInterfaces.

This mapping strategy translates a class to an IEnumerable<ServiceDescriptor>, with one item for each implemented interface as service and the class as its implementation.

Then, this collection is registered in DI by using an IRegistrationStrategy called Add.

This registration strategy simply adds each ServiceDescriptor returned by the mapping strategy into the ServiceCollection of the HostBuilder.

We can implement our own mapping and registration strategies and use them with our lifetime attributes.

Let’s develop this concept with an example of a Mapping Strategy that is already included in Registerly:

public class As<T>() : As(typeof(T))
{
}

public class As(Type serviceType) : IMappingStrategy
{
    public IEnumerable<ServiceDescriptor> Map(IEnumerable<Type> implementationTypes, ILifetimeStrategy lifetimeStrategy)
    {
        foreach (var implementationType in implementationTypes)
        {
            yield return new ServiceDescriptor(serviceType, implementationType, lifetimeStrategy.Map(implementationType));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

With this strategy we could register our previous ChatterboxService in a different manner:

[Scoped<As<IGreeter>>]
public class ChatterboxService: IGreeter, IFareweller
{
  public string Greet() => "Hi there!";
  public string Farewell() => "See ya later, alligator!";
}
Enter fullscreen mode Exit fullscreen mode

That attribute usage would register the service, but only as IGreeter, ignoring IFareweller.

This implies that, by implementing our own strategies, we could register our classes in any way we wanted to, besides the ones already provided by Registerly.

Bonus: Register classes by searching for them

Registering your services by using attributes is, by far, the easiest way to deal with your DI needs, but, sometimes, you don’t have access to the source code of the service you want to register, or you need to perform some kind of “bulk registration” of a big number of classes for which applying attributes would be cumbersome.

No worries, Registerly got you covered here, too.

As I mentioned, Registerly started by using the awesome library Scrutor to deal with the search and registration in DI of the classes that were marked with attributes but, eventually, the dependency on Scrutor was dropped, and the whole process is now implemented natively.

So, as in Scrutor, there’s a way to programmatically search for classes and to register them using any of the provided or custom strategies mentioned.

Consider the following fragment of code:

builder.Register(classes => classes
    .FromAssemblyOf<ChatterboxService>()
    .Where(c => c.Exactly<ChatterboxService>()));
Enter fullscreen mode Exit fullscreen mode

It does exactly the same as our initial sample. It registers ChatterboxService in DI with all its implemented interfaces.

Note that neither Add, Scoped, or AsImplementedInterfaces strategies appear, because they are the default strategies.

So, the preceding code is equivalent to the following one:

builder.Register(classes => classes
    .FromAssemblyOf<ChatterboxService>()
    .Where(c => c.Exactly<ChatterboxService>())
    .Using<Scoped, AsImplementedInterfaces, Add>());
Enter fullscreen mode Exit fullscreen mode

If we wanted to register the service as singleton instead, we’d need to state it explicitly:

builder.Register(classes => classes
    .FromAssemblyOf<ChatterboxService>()
    .Where(c => c.Exactly<ChatterboxService>())
    .Using<Singleton>);
Enter fullscreen mode Exit fullscreen mode

Once again, we don’t need to explicitly instruct the library to use the remaining default strategies.

Conclusion

Registerly is a powerful tool that simplifies the process of registering services in the dependency injection container using attributes. It provides a clean and intuitive way to manage DI registrations, reducing boilerplate code and improving maintainability.

By using Registerly, you can focus on writing your application logic without worrying about the complexities of DI registration. Whether you are building a small application or a large enterprise system, Registerly can help you streamline your DI setup and improve your development workflow.

Give Registerly a try and experience the benefits of simplified DI registration in your .NET applications. You can find more information and documentation on the Registerly documentation page.

Top comments (0)