Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
.NET admite el patrón de diseño de software de inserción de dependencias (DI), que es una técnica para lograr la inversión de control (IoC) entre las clases y sus dependencias. La inserción de dependencias en .NET es una parte integrada del marco, junto con la configuración, el registro y el patrón de opciones.
Una dependencia es un objeto del que depende otro objeto. La siguiente MessageWriter clase tiene un Write método en el que otras clases pueden depender de:
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Una clase puede crear una instancia de la MessageWriter clase para usar su Write método. En el ejemplo siguiente, la MessageWriter clase es una dependencia de la Worker clase :
public class Worker : BackgroundService
{
private readonly MessageWriter _messageWriter = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
En este caso, la Worker clase crea y depende directamente de la MessageWriter clase . Las dependencias codificadas de forma rígida como esta son problemáticas y deben evitarse por los siguientes motivos:
- Para reemplazar
MessageWriterpor una implementación diferente, debe modificar laWorkerclase . - Si
MessageWritertiene dependencias, laWorkerclase también debe configurarlas. En un proyecto grande con varias clases que dependen deMessageWriter, el código de configuración se dispersa por la aplicación. - Esta implementación es difícil para realizar pruebas unitarias. La aplicación debe usar una clase
MessageWritercomo boceto o código auxiliar, que no es posible con este enfoque.
El concepto
La inserción de dependencias soluciona problemas de dependencia codificados de forma rígida a través de:
Uso de una interfaz o clase base para abstraer la implementación de dependencias.
Registro de la dependencia en un contenedor de servicios.
.NET proporciona un contenedor de servicios integrado, IServiceProvider. Normalmente, los servicios se registran al iniciar la aplicación y se anexan a una IServiceCollection. Una vez agregados todos los servicios, use BuildServiceProvider para crear el contenedor de servicios.
Inserción del servicio en el constructor de la clase que lo utiliza.
El marco de trabajo asume la responsabilidad de crear una instancia de la dependencia y de desecharla cuando ya no es necesaria.
Sugerencia
En la terminología de inserción de dependencias, un servicio suele ser un objeto que proporciona un servicio a otros objetos, como el IMessageWriter servicio. El servicio no está relacionado con un servicio web, aunque podría usar un servicio web.
Por ejemplo, supongamos que la IMessageWriter interfaz define el Write método . Esta interfaz se implementa mediante un tipo concreto, , MessageWritermostrado anteriormente. El siguiente código de ejemplo registra el servicio IMessageWriter con el tipo concreto MessageWriter. El AddSingleton método registra el servicio con un ciclo de vida singleton, lo que significa que no se elimina hasta que la aplicación se cierra.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
// <SnippetMW>
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
// </SnippetMW>
// <SnippetIMW>
public interface IMessageWriter
{
void Write(string message);
}
// </SnippetIMW>
// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
// </SnippetWorker>
En el ejemplo de código anterior, las líneas resaltadas:
- Cree una instancia del generador de aplicaciones host.
- Configure los servicios registrando el
Workercomo un servicio hospedado y laIMessageWriterinterfaz como un servicio único con una implementación correspondiente de la claseMessageWriter. - Compile el host y ejecútelo.
El host contiene el proveedor de servicios de inserción de dependencias. También contiene el resto de servicios pertinentes que se requieren para crear instancias de Worker automáticamente y proporcionar la implementación de IMessageWriter correspondiente como argumento.
Mediante el patrón de inyección de dependencias, el servicio de trabajo no usa el tipo MessageWriter concreto, solo la interfaz IMessageWriter que implementa. Este diseño facilita el cambio de la implementación que usa el servicio de trabajo sin modificar el servicio de trabajo. El servicio de trabajo tampoco crea una instancia de MessageWriter. El contenedor de inserción de dependencias crea la instancia.
Ahora, imagine que desea reemplazar MessageWriter por un tipo que use el servicio de registro proporcionado por el framework. Cree una clase LoggingMessageWriter que dependa de ILogger<TCategoryName> solicitándolo en el constructor.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Para cambiar de MessageWriter a LoggingMessageWriter, basta con actualizar la llamada a AddSingleton para registrar esta nueva implementación de IMessageWriter.
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Sugerencia
El contenedor se resuelve aprovechando ILogger<TCategoryName>), lo que elimina la necesidad de registrar cada tipo construido (genérico).
Comportamiento de inserción de constructor
Los servicios se pueden resolver mediante IServiceProvider (el contenedor de servicios integrado) o ActivatorUtilities.
ActivatorUtilities crea objetos que no están registrados en el contenedor y se usan con algunas características del marco.
Los constructores pueden aceptar argumentos que no se proporcionan mediante la inserción de dependencias, pero los argumentos deben asignar valores predeterminados.
Cuando IServiceProvider o ActivatorUtilities resuelven servicios, la inserción de constructores requiere un constructor público.
Cuando ActivatorUtilities resuelve los servicios, la inserción de constructores requiere que solo exista un constructor aplicable. Se admiten las sobrecargas de constructor, pero solo puede existir una sobrecarga cuyos argumentos pueda cumplir la inserción de dependencias.
Reglas de selección de constructores
Cuando un tipo define más de un constructor, el proveedor de servicios tiene lógica para determinar qué constructor usar. Se selecciona el constructor con el mayor número de parámetros donde los tipos pueden resolver DI. Considere el siguiente servicio de ejemplo:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
En el código anterior, suponga que se ha agregado el registro de sucesos y es resoluble desde el proveedor de servicios, pero los tipos ServiceA y ServiceB no lo son. El constructor con el ILogger<ExampleService> parámetro resuelve la ExampleService instancia. Aunque hay un constructor que define más parámetros, los tipos ServiceA y ServiceB no son resolubles mediante DI.
Si hay ambigüedad al detectar constructores, se produce una excepción. Considere el siguiente servicio de ejemplo de C#:
Advertencia
Este ExampleService código con parámetros de tipo ambiguos, resolvibles por DI, produce una excepción.
No hagas esto: está pensado para mostrar lo que significa "tipos ambiguos resolubles por DI".
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
En el ejemplo anterior, hay tres constructores. El primer constructor no tiene parámetros y no requiere ningún servicio del proveedor de servicios. Suponga que tanto el registro como las opciones se han agregado al contenedor de DI y son servicios que se pueden resolver en DI. Cuando el contenedor de DI intenta resolver el tipo ExampleService, lanza una excepción debido a que los dos constructores son ambiguos.
Evite la ambigüedad mediante la definición de un constructor que acepte los dos tipos que se pueden resolver de di en su lugar:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Validación del ámbito
Los servicios con alcance son eliminados por el contenedor que los creó. Si se crea un servicio con ámbito en el contenedor raíz, la duración del servicio se promueve eficazmente a singleton porque el contenedor raíz solo lo elimina cuando se cierra la aplicación. Al validar los ámbitos de servicio, este tipo de situaciones se detectan cuando se llama a BuildServiceProvider.
Cuando una aplicación se ejecuta en el entorno de desarrollo y llama a CreateApplicationBuilder para compilar el host, el proveedor de servicios predeterminado realiza comprobaciones para comprobar que:
- Los servicios con ámbito no se resuelven desde el proveedor de servicios raíz.
- Los servicios con ámbito no se insertan en singletons.
Escenarios de ámbito
IServiceScopeFactory siempre se registra como singleton, pero IServiceProvider puede variar en función de la duración de la clase que lo contiene. Por ejemplo, si resuelve los servicios de un ámbito y cualquiera de esos servicios toma una IServiceProviderinstancia de , es una instancia con ámbito.
Para lograr los servicios de ámbito dentro de las implementaciones de IHostedService, como BackgroundService, no inserte las dependencias del servicio a través de la inserción de constructores. En su lugar, inyecte una instancia de IServiceScopeFactory, cree un ámbito y, a continuación, resuelva las dependencias del ámbito para usar la duración de servicio adecuada.
namespace WorkerScope.Example;
public sealed class Worker(
ILogger<Worker> logger,
IServiceScopeFactory serviceScopeFactory)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
try
{
logger.LogInformation(
"Starting scoped work, provider hash: {hash}.",
scope.ServiceProvider.GetHashCode());
var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
var next = await store.GetNextAsync();
logger.LogInformation("{next}", next);
var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
await processor.ProcessAsync(next);
logger.LogInformation("Processing {name}.", next.Name);
var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
await relay.RelayAsync(next);
logger.LogInformation("Processed results have been relayed.");
var marked = await store.MarkAsync(next);
logger.LogInformation("Marked as processed: {next}", marked);
}
finally
{
logger.LogInformation(
"Finished scoped work, provider hash: {hash}.{nl}",
scope.ServiceProvider.GetHashCode(), Environment.NewLine);
}
}
}
}
}
En el código anterior, mientras se ejecuta la aplicación, el servicio en segundo plano:
- Depende de IServiceScopeFactory.
- Crea un IServiceScope para resolver otros servicios.
- Resuelve los servicios con ámbito para su consumo.
- Trabaja en el procesamiento de objetos, los retransmite y, por último, los marca como procesados.
En el código fuente de ejemplo, puede ver cómo las implementaciones de IHostedService pueden beneficiarse de la duración del servicio con ámbito.
Servicios con claves
Puede registrar servicios y realizar búsquedas basadas en una clave. En otras palabras, es posible registrar varios servicios con claves diferentes y usar esta clave para la búsqueda.
Pongamos un caso en el que tiene implementaciones diferentes de la interfaz IMessageWriter: MemoryMessageWriter y QueueMessageWriter.
Puede registrar estos servicios mediante la sobrecarga de los métodos de registro de servicio (vistos anteriormente) que admiten una clave como parámetro:
services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");
key no se limita a string.
key puede ser cualquier object que desee, siempre y cuando el tipo implemente Equals correctamente.
En el constructor de la clase que usa IMessageWriter, agregue FromKeyedServicesAttribute para especificar la clave del servicio que se va a resolver:
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
Propiedad KeyedService.AnyKey
La propiedad KeyedService.AnyKey proporciona una clave especial para gestionar servicios con claves. Puede registrar un servicio mediante KeyedService.AnyKey como alternativa que coincide con cualquier clave. Esto resulta útil cuando desea proporcionar una implementación predeterminada para cualquier clave que no tenga un registro explícito.
var services = new ServiceCollection();
// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
// Create a cache instance based on the key.
return new DefaultCache(key?.ToString() ?? "unknown");
});
// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());
var provider = services.BuildServiceProvider();
// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");
// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");
var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");
En el ejemplo anterior:
- Solicitar
ICachecon clave"premium"devuelve la instanciaPremiumCache. - Solicitar
ICachecon cualquier otra clave (como"basic"o"standard") crea un nuevoDefaultCachemediante la alternativaAnyKey.
Importante
A partir de .NET 10, llamar a GetKeyedService() con KeyedService.AnyKey lanza una InvalidOperationException porque AnyKey está destinado como una opción de respaldo para registro, no como una clave de consulta. Para obtener más información, vea Solucionar problemas en GetKeyedService() y GetKeyedServices() con AnyKey.
Consulte también
- Inicio rápido: Conceptos básicos de inyección de dependencias
- Tutorial: Uso de la inserción de dependencias en .NET
- Instrucciones para la inserción de dependencias
- Inserción de dependencias en ASP.NET Core
- Patrones de la Conferencia NDC para el desarrollo de aplicaciones de inserción de dependencias
- Principio de dependencias explícitas
- Los contenedores de inversión de control y el patrón de inserción de dependencias (Martin Fowler)