Instrucciones para la inserción de dependencias

En este artículo se proporcionan instrucciones generales y procedimientos recomendados para implementar la inserción de dependencias 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 mediante el diseño de aplicaciones para usar servicios singleton en su lugar.
  • Evitar la creación directa de instancias de clases dependientes dentro de los servicios. La creación directa de instancias se acopla al código de una implementación particular.
  • 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 tipo descartable anterior está diseñado para tener una duración 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:

  • El contenedor de servicios no crea la instancia de ExampleService.
  • El marco de trabajo no elimina los servicios de manera automática.
  • El desarrollador es responsable de la eliminació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 habitualmente tendría un método Create que llama directamente al constructor del tipo final. Si el tipo final tiene otras dependencias, la fábrica puede:

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.

Instrucciones generales sobre IDisposable

  • No registre instancias de IDisposable con una duración transitoria. En su lugar, use el patrón de fábrica.
  • 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 este no es un patrón ideal.
  • La recepción de una dependencia de IDisposable a través de las inserciones de dependencias no requiere que el receptor implemente IDisposable por sí mismo. 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 más información sobre la limpieza de recursos, vea Implementación de un métodoDispose o Implementación de un método DisposeAsync. 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
  • Inyección basada en el nombre (solo .NET 7 y versiones anteriores). Para obtener más información, consulte Servicios con claves.
  • 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 de singleton tiene una dependencia en un servicio transitorio, es posible que este también deba ser seguro para subprocesos, según 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.

Recomendaciones

  • No se admite la resolución de servicio basada en async/await y Task. 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 las 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 BuildServiceProvider suele ocurrir cuando el desarrollador desea resolver un servicio al registrar otro servicio. En su lugar, use una sobrecarga que incluya el IServiceProvider por 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.

Al igual que sucede con todas las recomendaciones, podría verse en una situación que le obligue a ignorar alguna de ellas. Las excepciones son poco frecuentes, principalmente en casos especiales dentro del marco de trabajo mismo.

La inserción de dependencias es una alternativa a los patrones de acceso a objetos estáticos o globales. No podrá aprovechar 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 instrucciones de este artículo, hay varios antipatrones que debe evitar. Algunos de estos antipatrones son aprender del desarrollo de los propios tiempos de ejecución.

Advertencia

Estos son antipatrones de ejemplo: no copiar el código, no usar estos patrones y evitar estos patrones a toda costa.

El contenedor captura los servicios transitorios descartables

Cuando se registran servicios transitorios que implementan IDisposable, de forma predeterminada, el contenedor de DI incluirá estas referencias y ningún método Dispose() hasta que el contenedor se elimine cuando la aplicación se detenga si se resolvieron desde el contenedor, o hasta que se elimine el ámbito si se resolvieron desde un ámbito. Esto puede resultar en una fuga de memoria si se resuelve desde el nivel de contenedor.

static void TransientDisposablesWithoutDispose()
{
    var services = new ServiceCollection();
    services.AddTransient<ExampleDisposable>();
    ServiceProvider serviceProvider = services.BuildServiceProvider();

    for (int i = 0; i < 1000; ++i)
    {
        _ = serviceProvider.GetRequiredService<ExampleDisposable>();
    }

    // serviceProvider.Dispose();
}

En el antipatrón anterior, se crean instancias de 1000 objetos ExampleDisposable y se liberan. No se eliminarán hasta que se elimine la instancia de serviceProvider.

Para más información sobre cómo depurar fugas de memoria, vea Depuración de una fuga de memoria en .NET Core.

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 el generador es asincrónico y se usa Task<TResult>.Result, se producirá un interbloqueo.

static void DeadLockWithAsyncFactory()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>(implementationFactory: provider =>
    {
        Bar bar = GetBarAsync(provider).Result;
        return new Foo(bar);
    });

    services.AddSingleton<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    _ = serviceProvider.GetRequiredService<Foo>();
}

En el código anterior, se proporciona una expresión lambda a implementationFactory, donde el cuerpo llama a Task<TResult>.Result en un método de devolución 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).

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
    // Emulate asynchronous work operation
    await Task.Delay(1000);

    return serviceProvider.GetRequiredService<Bar>();
}

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" lo acuñó Mark Seemann y hace referencia a la configuración incorrecta de la duración de los servicios, donde un servicio de mayor duración mantiene una dependencia cautiva del servicio de menor duración.

static void CaptiveDependency()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    // Enable scope validation
    // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

    _ = serviceProvider.GetRequiredService<Foo>();
}

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 objeto Foo requiere un objeto Bar y, como Foo es un singleton y Bar su ámbito, se trata de una configuración incorrecta. Tal y como está, solo se crearía una instancia de Foo, y se mantendría en Bar mientras dure, que será un período mayor respecto a la duración con ámbito prevista de Bar. Considere la posibilidad de validar ámbitos, pasando validateScopes: true a BuildServiceProvider(IServiceCollection, Boolean). Al validar los ámbitos, obtendría un InvalidOperationException con un mensaje similar a "No se puede consumir el servicio 'Bar' con ámbito en el singleton 'Foo'".

Para más información, vea Validación del ámbito.

Servicio con ámbito como singleton

Al usar los servicios con ámbito, si no va a crear un ámbito o en un ámbito existente, el servicio se convierte en un singleton.

static void ScopedServiceBecomesSingleton()
{
    var services = new ServiceCollection();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
    using (IServiceScope scope = serviceProvider.CreateScope())
    {
        // Correctly scoped resolution
        Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
    }

    // Not within a scope, becomes a singleton
    Bar avoid = serviceProvider.GetRequiredService<Bar>();
}

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.

Vea también