Nota
O acceso a esta páxina require autorización. Pode tentar iniciar sesión ou modificar os directorios.
O acceso a esta páxina require autorización. Pode tentar modificar os directorios.
En este artículo se proporcionan instrucciones generales y procedimientos recomendados para implementar la inserción de dependencias (DI) en aplicaciones .NET.
Diseño de servicios para la inserción de dependencias
Al diseñar servicios para la inserción de dependencias:
- Evitar clases y miembros estáticos y con estado. Evitar crear un estado global al diseñar aplicaciones para usar servicios únicos en su lugar.
- Evitar la creación directa de instancias de clases dependientes dentro de los servicios. La instanciación directa acopla el código a una implementación específica.
- Cree servicios pequeños, bien factorizados y probados con facilidad.
Si una clase tiene muchas dependencias insertadas, podría ser un signo de que la clase tiene demasiadas responsabilidades e infringe el principio de responsabilidad única (SRP). Trate de mover algunas de las responsabilidades de la clase a clases nuevas para intentar refactorizarla.
Eliminación de servicios
El contenedor es responsable de la limpieza de los tipos que crea y llama a Dispose en las instancias de IDisposable. El desarrollador nunca debe eliminar los servicios resueltos desde el contenedor. Si un tipo o fábrica se registra como singleton, el contenedor elimina el singleton de manera automática.
En el ejemplo siguiente, el contenedor de servicios crea los servicios y se eliminan de manera automática:
namespace ConsoleDisposable.Example;
public sealed class TransientDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}
El producto desechable anterior está diseñado para tener una vida útil transitoria.
namespace ConsoleDisposable.Example;
public sealed class ScopedDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}
El tipo descartable anterior está diseñado para tener una duración con ámbito.
namespace ConsoleDisposable.Example;
public sealed class SingletonDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}
El tipo descartable anterior está diseñado para tener una duración de singleton.
using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();
using IHost host = builder.Build();
ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();
ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();
await host.RunAsync();
static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
Console.WriteLine($"{scope}...");
using IServiceScope serviceScope = services.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
_ = provider.GetRequiredService<TransientDisposable>();
_ = provider.GetRequiredService<ScopedDisposable>();
_ = provider.GetRequiredService<SingletonDisposable>();
}
En la consola de depuración se muestra el siguiente resultado de ejemplo después de la ejecución:
Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()
Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()
info: Microsoft.Hosting.Lifetime[0]
Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
SingletonDisposable.Dispose()
Servicios no creados por el contenedor de servicios
Observe el código siguiente:
// Register example service in IServiceCollection.
builder.Services.AddSingleton(new ExampleService());
En el código anterior:
- La instancia
ExampleServiceno es creada por el contenedor de servicios. - El marco de trabajo no elimina los servicios de manera automática.
- El desarrollador es responsable de la disposición de los servicios.
Instrucciones de IDisposable para instancias transitorias y compartidas
Instancia transitoria, duración limitada
Escenario
La aplicación requiere una instancia de IDisposable con una duración transitoria para cualquiera de estos escenarios:
- La instancia se resuelve en el ámbito raíz (contenedor raíz).
- La instancia se debe eliminar antes de que finalice el ámbito.
Solución
Use el patrón de fábrica para crear una instancia fuera del ámbito principal. En esta situación, la aplicación generalmente tiene un Create método que llama directamente al constructor del tipo final. Si el tipo final tiene otras dependencias, la fábrica puede:
- Recibir un IServiceProvider en su constructor.
- Usar ActivatorUtilities.CreateInstance para crear instancias de la instancia fuera del contenedor, mientras usa el contenedor para sus dependencias.
Instancia compartida, duración limitada
Escenario
La aplicación requiere una instancia de IDisposable compartida en varios servicios, pero la instancia de IDisposable debe tener una duración limitada.
Solución
Registre la instancia con una duración restringida. Use IServiceScopeFactory.CreateScope para crear un IServiceScope nuevo. Use el IServiceProvider del ámbito para obtener los servicios necesarios. Elimine el ámbito cuando ya no lo necesite.
Directrices generales IDisposable
- No registre instancias de IDisposable con una duración transitoria. Use el patrón de fábrica en su lugar para que el servicio resuelto se pueda eliminar manualmente cuando ya no esté en uso.
- No resuelva instancias de IDisposable con una duración transitoria o restringida en el ámbito raíz. La única excepción a esto es si la aplicación crea o vuelve a crear y elimina IServiceProvider, pero esto no es un patrón ideal.
- La recepción de una dependencia de IDisposable a través de DI no requiere que el receptor implemente IDisposable por sí solo. El receptor de la dependencia de IDisposable no debe llamar a Dispose en esa dependencia.
- Use ámbitos para controlar las duraciones de los servicios. Los ámbitos no son jerárquicos y no hay ninguna conexión especial entre ellos.
Para obtener más información sobre la limpieza de recursos, consulte Implementación de un Dispose método o Implementación de un DisposeAsync método. Además, tenga en cuenta el escenario Servicios transitorios descartables capturados por el contenedor por su relación con la limpieza de recursos.
Reemplazo del contenedor de servicios predeterminado
El contenedor de servicios integrado está diseñado para atender las necesidades del marco y de la mayoría de las aplicaciones de consumidor. Se recomienda usar el contenedor integrado a menos que se necesite una característica específica no admitida por el contenedor, como:
- Inserción de propiedades
- Contenedores secundarios
- Administración personalizada del ciclo de vida
- Compatibilidad con
Func<T>para la inicialización diferida - Registro basado en convenciones
Los siguientes contenedores de terceros se pueden usar con aplicaciones ASP.NET Core:
Seguridad para subprocesos
Cree servicios de singleton seguros para subprocesos. Si un servicio singleton tiene una dependencia de un servicio transitorio, el servicio transitorio también podría requerir seguridad de subprocesos en función de cómo lo use el singleton. El patrón de diseño Factory Method de un servicio único, como el segundo argumento para AddSingleton<TService > (IServiceCollection, Func<IServiceProvider,TService>), no necesita ser seguro para subprocesos. Al igual que un constructor de tipos (static), se garantiza que se le llame solo una vez mediante un único subproceso.
Además, el proceso de resolución de servicios desde el contenedor integrado de inserción de dependencias de .NET es seguro para subprocesos.
Una vez que se ha construido un IServiceProvider o IServiceScope, es seguro resolver los servicios desde varios subprocesos simultáneamente.
Nota:
La seguridad de subprocesos del propio contenedor DI solo garantiza que la construcción y resolución de servicios es segura. No consigue que las instancias de servicio ya resueltas sean seguras para los subprocesos.
Cualquier servicio (especialmente singletons) que contenga el estado mutable compartido debe implementar su propia lógica de sincronización si se accede simultáneamente.
Recomendaciones
- No se admite la resolución de servicio basada en
async/awaityTask. Como C# no admite constructores asincrónicos, use los métodos asincrónicos después de resolver sincrónicamente el servicio. - Evite almacenar datos y configuraciones directamente en el contenedor de servicios. Por ejemplo, el carro de la compra de un usuario no debería agregarse al contenedor de servicios. La configuración debe usar el patrón de opciones. Del mismo modo, evite los objetos de tipo "contenedor de datos" que solo existen para permitir el acceso a otro objeto. Es mejor solicitar el elemento real que se necesita mediante la inserción de dependencias.
- Evite el acceso estático a los servicios. Por ejemplo, evite capturar IApplicationBuilder.ApplicationServices como campo estático o propiedad para su uso en otra parte.
- Mantenga los generadores de DI rápidos y sincrónicos.
- Evite el uso del patrón del localizador de servicios. Por ejemplo, no invoque a GetService para obtener una instancia de servicio si puede usar la inserción de dependencias en su lugar.
- Otra variación del localizador de servicios que se debe evitar es insertar una fábrica que resuelva dependencias en tiempo de ejecución. Estas dos prácticas combinan estrategias de Inversión de control.
- Evite llamadas a BuildServiceProvider al configurar servicios. La llamada a
BuildServiceProvidersuele ocurrir cuando el desarrollador desea resolver un servicio al registrar otro servicio. En su lugar, use una sobrecarga que incluya elIServiceProviderpor este motivo. - El contenedor captura los servicios transitorios descartables para su eliminación. Esto puede resultar en una fuga de memoria si se resuelve desde el contenedor de nivel superior.
- Habilite la validación con ámbito para asegurarse de que la aplicación no tenga singletons que capturen los servicios con ámbito. Para más información, vea Validación del ámbito.
- Use únicamente la duración del ciclo de vida singleton para los servicios con su propio estado que sea costoso de crear o de compartir a nivel global. Evite usar el ciclo de vida de singleton para los servicios que no tienen ningún estado en sí mismos. La mayoría de los contenedores de IoC de .NET usan "Transitorio" como ámbito predeterminado. Consideraciones y desventajas de los singletons:
- Seguridad de subprocesos: un singleton debe implementarse de forma segura para subprocesos.
- Acoplamiento: puede acoplar solicitudes no relacionadas.
- Desafíos de pruebas: el estado compartido y el acoplamiento pueden dificultar las pruebas unitarias.
- Impacto en la memoria: un singleton puede mantener un grafo de objetos grande activo en la memoria durante la vida útil de la aplicación.
- Tolerancia a errores: si se produce un error en un singleton o en cualquier parte de su árbol de dependencias, no se puede recuperar fácilmente.
- Recarga de configuración: Los Singletons generalmente no pueden admitir la "recarga en caliente" de los valores de configuración.
- Pérdida de ámbito: un singleton puede capturar accidentalmente dependencias de ámbito o transitorias, promoviendo efectivamente estas dependencias al estado de singleton y causando efectos secundarios no deseados.
- Sobrecarga de inicialización: al resolver un servicio, el contenedor de IoC debe buscar la instancia singleton. Si aún no existe, debe crearlo de forma segura para hilos. Por el contrario, un servicio transitorio sin estado es muy barato de crear y destruir.
Al igual que todos los conjuntos de recomendaciones, es posible que pueda encontrar situaciones en las que sea necesario ignorar una recomendación. Las excepciones son poco frecuentes y son principalmente casos especiales dentro del propio marco.
La inserción de dependencias es una alternativa a los patrones de acceso a objetos estáticos o globales. Es posible que no se dé cuenta de las ventajas de la inserción de dependencias si la combina con el acceso a objetos estáticos.
Antipatrones de ejemplo
Además de las directrices de este artículo, hay varios anti patrones que debe evitar. Algunos de estos antipatrones son aprender del desarrollo de los propios tiempos de ejecución.
Advertencia
Estos son ejemplos de antipatrones. No copie el código, no use estos patrones y evite estos patrones a todos los costos.
El contenedor captura los servicios transitorios descartables
Al registrar servicios transitorios que implementan IDisposable, de forma predeterminada, el contenedor de inyección de dependencias retiene estas referencias. No los elimina hasta que el contenedor se elimina cuando la aplicación se detiene si se resuelven desde el contenedor, o hasta que el ámbito se elimina si se resolvieron desde un ámbito. Una pérdida de memoria puede producirse si se intenta resolver desde el nivel de contenedor.
En el antipatrón anterior, se crean instancias de 1000 objetos ExampleDisposable y se liberan. No se eliminarán hasta que se elimine la serviceProvider instancia.
Para más información sobre cómo depurar fugas de memoria, consulte Depure una fuga de memoria en .NET.
Los generadores de DI asincrónicos pueden producir interbloqueos
El término "generadores de DI" hace referencia a los métodos de sobrecarga que existen al llamar a Add{LIFETIME}. Hay sobrecargas que aceptan un Func<IServiceProvider, T>, donde T es el servicio que se está registrando y el parámetro se denomina implementationFactory.
implementationFactory se puede proporcionar como una expresión lambda, una función local o un método. Si la factoría es asincrónica y usa Task<TResult>.Result, provocará un interbloqueo.
En el código anterior, se asigna una expresión lambda a implementationFactory, donde el cuerpo llama a Task<TResult>.Result en un método que devuelve Task<Bar>. Esto provoca un interbloqueo. El método GetBarAsync simplemente emula una operación de trabajo asincrónica con Task.Delay y, a continuación, llama a GetRequiredService<T>(IServiceProvider).
Para más información sobre la programación asincrónica, vea Programación asincrónica: Consejos e información importante. Para más información sobre la depuración de interbloqueos, vea Depuración de un interbloqueo en .NET Core.
Cuando se ejecuta este antipatrón y se produce el interbloqueo, puede ver los dos subprocesos en espera de la ventana Pilas paralelas de Visual Studio. Para más información, vea Visualización de subprocesos y tareas en la ventana Pilas paralelas.
Dependencia cautiva
El término "dependencia cautiva", acuñado por Mark Seemann, hace referencia a la configuración incorrecta de los tiempos de vida de servicio, donde un servicio de larga duración mantiene cautivo a un servicio de vida más corta.
En el código anterior, Foo se registra como singleton y Bar tiene el ámbito, que en la superficie parece válido. Sin embargo, tenga en cuenta la implementación de Foo.
namespace DependencyInjection.AntiPatterns;
public class Foo(Bar bar)
{
}
El Foo objeto requiere un Bar objeto y, dado que Foo es un singleton y Bar tiene ámbito, se trata de una configuración incorrecta. Tal como está, Foo solo se instancia una vez y retiene Bar durante su vida útil, que es mayor que la duración de vida útil planificada de ámbito de Bar. Considere validar alcances pasando validateScopes: true a BuildServiceProvider(IServiceCollection, Boolean). Al validar los ámbitos, obtendrá un InvalidOperationException con un mensaje similar a "No se puede consumir el servicio con ámbito "Bar" de singleton "Foo".
Para más información, vea Validación del ámbito.
Servicio con ámbito como singleton
Al usar servicios con ámbito, si no está creando un ámbito ni está dentro de un ámbito existente, el servicio se convierte en un singleton.
En el código anterior, Bar se recupera dentro de un IServiceScope, que es correcto. El antipatrón es la recuperación de Bar fuera del ámbito, y la variable se denomina avoid para mostrar qué recuperación de ejemplo es incorrecta.