Tutorial: Uso de la inserción de dependencias en .NET

En este tutorial se muestra cómo usar la inserción de dependencias (DI) en .NET. Con las Extensiones de Microsoft, la inserción de dependencias se administra mediante la adición de servicios y su configuración en IServiceCollection. La interfaz de IHost expone la instancia de IServiceProvider, que actúa como un contenedor de todos los servicios registrados.

En este tutorial aprenderá a:

  • Crear una aplicación de consola de .NET que use la inserción de dependencias.
  • Compilar y configurar un host genérico.
  • Escribir varias interfaces y las implementaciones correspondientes.
  • Usar la duración y el ámbito del servicio para DI.

Requisitos previos

  • SDK de .NET Core 3.1 o versiones posteriores.
  • Familiaridad con la creación de nuevas aplicaciones .NET y la instalación de paquetes NuGet.

Creación de una nueva aplicación de consola

Mediante el comando dotnet new o un asistente para nuevo proyecto IDE, cree una nueva aplicación de consola .NET denominada ConsoleDIExample . Agregue el paquete NuGet Microsoft.Extensions.Hosting al proyecto.

El nuevo archivo de proyecto de aplicación de consola debe ser similar al siguiente:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

Importante

En este ejemplo, el paquete NuGet Microsoft.Extensions.Hosting es necesario para compilar y ejecutar la aplicación. Algunos metapaquetes pueden contener el paquete Microsoft.Extensions.Hosting, en cuyo caso no se requiere una referencia de paquete explícita.

Adición de interfaces

En esta aplicación de ejemplo, aprenderá cómo la inserción de dependencias controla la duración del servicio. Creará varias interfaces que representan diferentes duraciones de servicio. Agregue las siguientes interfaces al directorio raíz del proyecto:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

La interfaz IReportServiceLifetime define:

  • Una propiedad Guid Id que representa el identificador único del servicio.
  • Una propiedad ServiceLifetime que representa la duración del servicio.

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

Todas las interfaces secundarias de IReportServiceLifetime implementan explícitamente IReportServiceLifetime.Lifetime con un valor predeterminado. Por ejemplo, IExampleTransientService implementa explícitamente IReportServiceLifetime.Lifetime con el valor ServiceLifetime.Transient.

Adición de las implementaciones predeterminadas

Todas las implementaciones de ejemplo inicializan su propiedad Id con el resultado de Guid.NewGuid(). Agregue las siguientes clases de implementación predeterminadas para los distintos servicios al directorio raíz del proyecto:

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

Cada implementación se define como internal sealed e implementa su interfaz correspondiente. Por ejemplo, ExampleSingletonService implementa IExampleSingletonService.

Adición de un servicio que requiera DI

Agregue la siguiente clase de informador de duración de servicio, que actúa como un servicio para la aplicación de consola:

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter(
    IExampleTransientService transientService,
    IExampleScopedService scopedService,
    IExampleSingletonService singletonService)
{
    public void ReportServiceLifetimeDetails(string lifetimeDetails)
    {
        Console.WriteLine(lifetimeDetails);

        LogService(transientService, "Always different");
        LogService(scopedService, "Changes only with lifetime");
        LogService(singletonService, "Always the same");
    }

    private static void LogService<T>(T service, string message)
        where T : IReportServiceLifetime =>
        Console.WriteLine(
            $"    {typeof(T).Name}: {service.Id} ({message})");
}

ServiceLifetimeReporter define un constructor que requiere cada una de las interfaces de servicio mencionadas anteriormente, es decir, IExampleTransientService, IExampleScopedService y IExampleSingletonService. El objeto expone un método único que permite al consumidor informar del servicio con un parámetro lifetimeDetails determinado. Cuando se invoca, el método ReportServiceLifetimeDetails registra el identificador único de cada servicio con el mensaje de duración de servicio. Los mensajes de registro ayudan a visualizar la duración del servicio.

Registro de servicios para DI

Actualice Program.cs con el siguiente código:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ConsoleDI.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();

using IHost host = builder.Build();

ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");

await host.RunAsync();

static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
    using IServiceScope serviceScope = hostProvider.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;
    ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine();
}

Cada método de extensión services.Add{LIFETIME}<{SERVICE}> agrega servicios (y potencialmente los configura). Se recomienda que las aplicaciones sigan esta convención. No coloque métodos de extensión en el espacio de nombres Microsoft.Extensions.DependencyInjection a menos que cree un paquete oficial de Microsoft. Métodos de extensión definidos en el espacio de nombres Microsoft.Extensions.DependencyInjection:

  • Se muestran en IntelliSense sin necesidad de bloques using adicionales.
  • Reduzca el número de instrucciones using necesarias en las clases Program o Startup en las que se suele llamar a estos métodos de extensión.

La aplicación:

Conclusión

En esta aplicación de ejemplo, ha creado varias interfaces y las implementaciones correspondientes. Cada uno de estos servicios se identifica de forma única y se empareja con un ServiceLifetime. La aplicación de ejemplo muestra cómo registrar implementaciones de servicio en una interfaz y cómo registrar clases puras sin interfaces de respaldo. A continuación, la aplicación de ejemplo muestra cómo se resuelven las dependencias definidas como parámetros de constructor en tiempo de ejecución.

Cuando se ejecuta la aplicación, muestra una salida similar a lo siguiente:

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

En la salida de la aplicación, puede ver lo siguiente:

  • Los servicios Transient siempre son diferentes y se crea una nueva instancia con cada recuperación del servicio.
  • Los servicios Scoped solo cambian con un nuevo ámbito pero son la misma instancia dentro de un ámbito.
  • Los servicios Singleton siempre son los mismos y una instancia nueva solo se crea una vez.

Vea también