This approach leverages .NET Reflection to automatically register and decorate services, enhancing the maintainability and extensibility of ASP.NET Core applications through dependency injection.
Services are the foundation of every ASP.NET Core application and are used through dependency injection. As an application grows, managing services becomes more complex and time-consuming. The following approach uses .NET Reflection to automatically register and decorate services.
Service registration improves the maintainability and extensibility of the software. It allows an existing service to be replaced by a service with additional functionality without having to change it. Examples include caching of data, pre- or post-processing of data, or switching between different implementation variants.
To use this tool, you must have the ServiceRegistration.NET
NuGet package installed.
> dotnet add package ServiceRegistration.NET
Register the service registration in the DI container.
using ServiceRegistration.Service;
...
builder.Services.RegisterServices();
...
The ServiceSingleton
, ServiceScoped
, and ServiceTransient
attributes are used to define the service interface in the service implementation. The attribute correlates with the lifetime of the .NET dependency injection.
For decorators with multiple interface declarations, the IRepository<User>
service type must be explicitly specified.
[ServiceTransient(IRepository<User>)]
public class UserRepository : IRepository<User>, IDisposable
{
...
}
The ServiceDecorator
attribute is used to specify the type of UserRepository
to decorate in the UserCacheRepository
decorator class. When the service is registered, the following steps are performed in the DI:
- Register the type of
UserRepository
to decorate. - Register the service decorator
IServiceDecorator<UserRepository>
with ServiceDecorator<UserRepository>
. - Replace the
IRepository<User>
service registration of UserRepository
with UserCacheRepository
.
The ServiceDecorator<T>
is a proxy type for the decorated type, available through the Implementation
property.
Decoration can be applied to types that have a Service*
attribute, or be a service decorator itself for nested scenarios. The lifecycle of registered services is determined by the service attribute.
With the extension method IServiceCollection.DecorateService<TService, TDecorator, TComponent>
it is possible to decorate a service by code.
using ServiceRegistration.Service;
...
builder.Services.DecorateService<IRepository<User>, UserCacheRepository, UserRepository>();
...
Service registration can be controlled using several options.
- Filter the reflection scan for assemblies and assembly types.
- Resolve service registration conflicts.
- Customize the target registration type.
By default, all assemblies of the current domain are scanned, which can be restricted using the AssemblyFilter
. Types are restricted using the TypeFilter
, where types with the ServiceIgnore
attribute are generally ignored. In the following example, only IRepository
service types are registered.
builder.Services.RegisterServices(new()
{
TypeFilter = type => type.GetInterfaces().Any(x <span class="pl-c1">=></span>
x<span class="pl-kos">.IsGenericType &&</span>
x<span class="pl-kos">.GetGenericTypeDefinition() == typeof(IRepository<>))</span>
});
If there are multiple registrations for a service, the conflicts must be resolved with the ResolveRegistration
function or a runtime error will occur.
builder.Services.RegisterServices(new()
{
ResolveRegistration = (type, filetime, conflicts) => conflicts.Last()
});
Before the services are registered in the DI, the MapRegistration
function provides the ability to customize the type registrations.
builder.Services.RegisterServices(new()
{
MapRegistration = registration => registration.ImplementationType
});