La Inyección de Dependencias (DI) es un pilar fundamental en el desarrollo moderno con .NET, permitiendo desacoplar componentes y facilitar la inversión de control (IoC). Aunque el framework proporciona un contenedor nativo, registrar manualmente cada servicio puede volverse una tarea tediosa y propensa a errores en proyectos de gran escala.
Ciclos de vida en .NET
Antes de automatizar el registro, es crucial comprender los tres ciclos de vida principales que gestiona el contenedor de servicios:
- Transient (Sujeto a cambios): Se crea una nueva instancia cada vez que se solicita el servicio. Es ideal para componentes ligeros y sin estado.
- Scoped (De ámbito): Se crea una instancia única por cada solicitud HTTP. Es el estándar para el manejo de contextos de bases de datos o repositorios dentro de un mismo flujo de petición.
- Singleton (Único): La instancia se crea la primera vez que se solicita y permanece activa durante toda la vida de la aplicación. Se utiliza para configuraciones globales o servicios de caché.
1. Automatización mediante Reflexión e Interfaces Marcadoras
Una técnica común consiste en definir interfaces vacías (marcadoras) que identifiquen el ciclo de vida deseado para cada clase. Mediante reflexión, podemos escanear los ensamblajes y registrar las implementaciones automáticamente.
// Definición de interfaces marcadoras
public interface IScopedService { }
public interface ISingletonService { }
public interface ITransientService { }
public static class DependencyInjectionExtensions
{
public static IServiceCollection RegisterAllServices(this IServiceCollection services)
{
var mainAssembly = Assembly.GetEntryAssembly();
// Obtener tipos de los ensamblajes referenciados y el actual
var allTypes = mainAssembly!.GetReferencedAssemblies()
.Select(Assembly.Load)
.Concat(new[] { mainAssembly })
.SelectMany(a => a.GetTypes())
.Where(t => t.IsClass && !t.IsAbstract)
.Distinct();
var serviceDefinitions = allTypes.Where(t =>
typeof(ITransientService).IsAssignableFrom(t) ||
typeof(IScopedService).IsAssignableFrom(t) ||
typeof(ISingletonService).IsAssignableFrom(t));
foreach (var implementationType in serviceDefinitions)
{
// Buscamos la interfaz principal que no sea la marcadora
var serviceInterface = implementationType.GetInterfaces()
.FirstOrDefault(i => i != typeof(ITransientService) &&
i != typeof(IScopedService) &&
i != typeof(ISingletonService) &&
!i.IsGenericType);
if (serviceInterface == null) continue;
if (typeof(ITransientService).IsAssignableFrom(implementationType))
services.AddTransient(serviceInterface, implementationType);
else if (typeof(IScopedService).IsAssignableFrom(implementationType))
services.AddScoped(serviceInterface, implementationType);
else if (typeof(ISingletonService).IsAssignableFrom(implementationType))
services.AddSingleton(serviceInterface, implementationType);
}
return services;
}
}
Para activarlo, simplemente se invoca en el archivo Program.cs: builder.Services.RegisterAllServices();.
2. Registro basado en Escaneo de Archivos y Convenciones
Este enfoque es útil cuando los servicios están aislados en bibliotecas de clases externas (DLL) y se desea cargar todo lo que cumpla con una convención de nomenclatura específica (por ejemplo, que terminen en "Repository").
public static IServiceCollection AddServicesByConvention(this IServiceCollection services, string assemblySuffix)
{
var executionPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var targetDll = Path.Combine(executionPath!, $"{assemblySuffix}.dll");
if (!File.Exists(targetDll)) return services;
var assembly = Assembly.LoadFrom(targetDll);
var targetTypes = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Service"));
foreach (var type in targetTypes)
{
var contract = type.GetInterfaces().FirstOrDefault(i => i.Name == $"I{type.Name}");
if (contract == null) continue;
// Registro predeterminado como Scoped basándose en marcadores o lógica personalizada
if (typeof(IScopedService).IsAssignableFrom(type))
services.AddScoped(contract, type);
else if (typeof(ITransientService).IsAssignableFrom(type))
services.AddTransient(contract, type);
}
return services;
}
3. Uso de la biblioteca Scrutor
Scrutor es la solución más robusta y extendida en el ecosistema .NET para el escaneo de ensamblajes. Proporciona un API fluido que simplifica drásticamente el proceso de registro dinámico.
// Instalación vía NuGet: Install-Package Scrutor
services.Scan(selector => selector
// 1. Localizar el ensamblaje base
.FromAssemblyOf<IApplicationMarker>()
// 2. Filtrar y registrar servicios transitorios
.AddClasses(c => c.AssignableTo<ITransientService>())
.AsImplementedInterfaces()
.WithTransientLifetime()
// 3. Filtrar y registrar servicios con ámbito (Scoped)
.AddClasses(c => c.AssignableTo<IScopedService>())
.AsSelfWithInterfaces()
.WithScopedLifetime()
// 4. Soporte para tipos genéricos abiertos
.AddClasses(c => c.AssignableTo(typeof(IRepository<>)))
.AsImplementedInterfaces()
.WithScopedLifetime()
);
Scrutor no solo reduce la cantidad de código repetitivo, sino que también maneja escenarios complejos como el registro de múltiples interfaces para una sola implementación o la aplicación de decoradores de forma nativa.