Inserción de dependencias en ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

De Kirk Larkin, Steve Smith y Brandon Dahler

ASP.NET Core admite el patrón de diseño de software de inserción de dependencias (DI), que es una técnica para conseguir la inversión de control (IoC) entre clases y sus dependencias.

Para más información específica de la inserción de dependencias en controladores de MVC, consulte Inserción de dependencias en controladores en ASP.NET Core.

Para obtener información sobre el uso de la inserción de dependencias en aplicaciones que no son aplicaciones web, consulte Inserción de dependencias en .NET.

Para más información sobre la inserción de dependencias de opciones, consulte Patron de opciones en ASP.NET Core.

En este tema se proporciona información sobre la inyección de dependencias en ASP.NET Core. La documentación principal sobre el uso de la inserción de dependencias se incluye en Inserción de dependencias en .NET.

Vea o descargue el código de ejemplo (cómo descargarlo)

Información general sobre la inserción de dependencias

Una dependencia es un objeto del que depende otro objeto. Examine la clase MyDependency siguiente con un método WriteMessage del que dependen otras clases:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Una clase puede crear una instancia de la clase MyDependency para usar su método WriteMessage. En el ejemplo siguiente, la clase MyDependency es una dependencia de la clase IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

La clase crea y depende directamente de la clase MyDependency. Las dependencias de código, como en el ejemplo anterior, son problemáticas y deben evitarse por las razones siguientes:

  • Para reemplazar MyDependency por una implementación diferente, se debe modificar la clase IndexModel.
  • Si MyDependency tiene dependencias, deben configurarse según la clase IndexModel. En un proyecto grande con varias clases que dependen de MyDependency, el código de configuración se dispersa por la aplicación.
  • Esta implementación es difícil para realizar pruebas unitarias.

La inserción de dependencias aborda estos problemas mediante:

  • Uso de una interfaz o clase base para abstraer la implementación de dependencias.
  • Registro de la dependencia en un contenedor de servicios. ASP.NET Core proporciona un contenedor de servicios integrado, IServiceProvider. Por lo general, los servicios se registran en el archivo Program.cs de la aplicación.
  • Inserción del servicio en el constructor de la clase en la que se usa. El marco de trabajo asume la responsabilidad de crear una instancia de la dependencia y de desecharla cuando ya no es necesaria.

En la aplicación de ejemplo, la interfaz IMyDependency define el método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Esta interfaz se implementa mediante un tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

La aplicación de ejemplo registra el servicio IMyDependency con el tipo concreto MyDependency. El método AddScoped registra el servicio mediante una duración con ámbito, definida como la duración de una única solicitud. Las duraciones del servicio se describen más adelante en este tema.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

En la aplicación de ejemplo, el servicio IMyDependency se solicita y usa para llamar al método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Usando el patrón de DI, el controlador o la página de Razor:

  • No usa el tipo concreto MyDependency, solo la interfaz IMyDependency que implementa. Esto facilita el cambio de la implementación sin modificar el controlador ni la página de Razor.
  • No crea una instancia de MyDependency, la crea el contenedor de DI.

La implementación de la interfaz de IMyDependency se puede mejorar mediante el uso de la API de registro integrada:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

El objeto Program.cs actualizado registra la nueva implementación de IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 depende de ILogger<TCategoryName>, el que solicita en el constructor. ILogger<TCategoryName> es un servicio proporcionado por el marco de trabajo.

No es raro usar la inserción de dependencias de forma encadenada. Cada dependencia solicitada a su vez solicita sus propias dependencias. El contenedor resuelve las dependencias del gráfico y devuelve el servicio totalmente resuelto. El conjunto colectivo de dependencias que deben resolverse suele denominarse árbol de dependencias, gráfico de dependencias o gráfico de objetos.

El contenedor resuelve ILogger<TCategoryName> aprovechando las ventajas de los tipos abiertos (genéricos), lo que elimina la necesidad de registrar todos los tipos construidos (genéricos).

En la terminología de la inserción de dependencias, un servicio:

  • Por lo general, es un objeto que proporciona un servicio a otros objetos, como el servicio IMyDependency.
  • No está relacionado con un servicio web, aunque el servicio puede utilizar un servicio web.

El marco de trabajo proporciona un sistema de registro sólido. Las implementaciones de IMyDependency que se muestran en los ejemplos anteriores se escribieron para mostrar la inserción de DI básica, no para implementar el registro. La mayoría de las aplicaciones no deberían tener que escribir registradores. En el código siguiente se muestra cómo usar el registro predeterminado, que no requiere que los servicios se registren:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Con el código anterior, no es necesario actualizar Program.cs porque el registro se proporciona a través del marco de trabajo.

Registro de grupos de servicios con métodos de extensión

El marco ASP.NET Core usa una convención para registrar un grupo de servicios relacionados. La convención es usar un único método de extensión de Add{GROUP_NAME} para registrar todos los servicios requeridos por una característica de marco. Por ejemplo, el método de extensión AddControllers registra los servicios necesarios para los controladores MVC.

El código siguiente lo genera la plantilla de Razor Pages con cuentas de usuario individuales y muestra cómo agregar servicios adicionales al contenedor mediante los métodos de extensión AddDbContext y AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Tenga en cuenta el siguiente método que registra los servicios y configura las opciones:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Los grupos de registros relacionados pueden moverse a un método de extensión para registrar los servicios. Por ejemplo, los servicios de configuración se agregan a la siguiente clase:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Los servicios restantes se registran en una clase similar. El siguiente código usa los nuevos métodos de extensión para registrar los servicios:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Nota: Cada método de extensión services.Add{GROUP_NAME} agrega servicios y potencialmente los configura. Por ejemplo, AddControllersWithViews agrega los controladores MVC de servicios con las vistas que se requieren y AddRazorPages agrega los servicios que Razor Pages requiere.

Duraciones de servicios

Consulte Duración del servicio en Inserción de dependencias en .NET.

Para usar servicios con ámbito en middleware, use uno de los enfoques siguientes:

  • Inserte el servicio en el método Invoke o InvokeAsync del middleware. El uso de la inserción de constructores genera una excepción en tiempo de ejecución porque obliga al servicio con ámbito a comportarse como un singleton. El ejemplo que aparece en la sección Opciones de registro y duración muestra el enfoque InvokeAsync.
  • Use middleware basado en Factory. El middleware registrado con este enfoque se activa por solicitud de cliente (conexión), lo que permite insertar servicios con ámbito en el constructor del middleware.

Para obtener más información, vea Escritura de middleware de ASP.NET Core personalizado.

Métodos de registro del servicio

Consulte Métodos de registro del servicio en Inserción de dependencias en .NET.

Es habitual usar varias implementaciones al utilizar tipos de simulación para las pruebas.

El registro de un servicio con un solo tipo de implementación es equivalente al registro de ese servicio con la misma implementación y el mismo tipo de servicio. Por eso no se pueden registrar varias implementaciones de un servicio mediante los métodos que no toman un tipo de servicio explícito. Estos métodos pueden registrar varias instancias de un servicio, pero todos tienen el mismo tipo de implementación.

Cualquiera de los métodos de registro de servicio anteriores se puede usar para registrar varias instancias de servicio del mismo tipo de servicio. En el ejemplo siguiente se llama a AddSingleton dos veces con IMyDependency como tipo de servicio. La segunda llamada a AddSingleton invalida la anterior cuando se resuelve como IMyDependency, y se agrega a la anterior cuando varios servicios se resuelven mediante IEnumerable<IMyDependency>. Los servicios aparecen en el orden en que se han registrado al resolverse mediante IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Servicios con claves

Los servicios con claves hacen referencia a un mecanismo para registrar y recuperar servicios de inserción de dependencias (DI) mediante claves. Un servicio se asocia a una clave llamando AddKeyedSingleton a (o AddKeyedScoped o AddKeyedTransient) para registrarlo. Acceda a un servicio registrado especificando la clave con el atributo [FromKeyedServices]. En el código siguiente se muestra cómo usar servicios con claves:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

Comportamiento de inserción de constructor

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

Contextos de Entity Framework

De manera predeterminada, los contextos de Entity Framework se agregan al contenedor de servicios mediante la duración con ámbito porque las operaciones de base de datos de aplicación web se suelen limitar a la solicitud de cliente. Para usar una duración distinta, especifíquela mediante el uso de una sobrecarga de AddDbContext. En los servicios de una duración determinada no se debe usar un contexto de base de datos con una duración más corta que la del servicio.

Opciones de registro y duración

Para mostrar la diferencia entre las duraciones del servicio y sus opciones de registro, considere las interfaces siguientes que representan una tarea como una operación con un identificador, OperationId. Según cómo esté configurada la duración de un servicio de operaciones para las interfaces siguientes, el contenedor proporciona las mismas instancias del servicio u otras distintas cuando así lo solicita la clase:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

La clase Operation siguiente implementa todas las interfaces anteriores. El constructor Operation genera un GUID y almacena los 4 últimos caracteres en la propiedad OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

El siguiente código crea varios registros de la clase Operation de acuerdo con las duraciones mencionadas:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

La aplicación de ejemplo muestra las duraciones de los objetos dentro y entre las solicitudes. IndexModel y el middleware solicitan cada clase del tipo IOperation y registran el valor de OperationId de cada una:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

De manera similar al IndexModel, el middleware resuelve los mismos servicios:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Los servicios con ámbito y transitorios se deben resolver en el método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

La salida del registrador muestra:

  • Los objetos Transient siempre son diferentes. El valor OperationId transitorio es distinto en el IndexModel y en el middleware.
  • Los objetos con ámbito son iguales para una determinada solicitud, pero varían entre solicitudes.
  • Los objetos singleton son los mismos para cada solicitud.

Para reducir la salida del registro, establezca "Logging:LogLevel:Microsoft:Error" en el archivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Resolución de un servicio al iniciar la aplicación

El código siguiente muestra cómo resolver un servicio con ámbito durante un tiempo limitado cuando se inicia la aplicación:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validación del ámbito

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

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

Servicios de solicitud

Los servicios y sus dependencias dentro de ASP.NET Core solicitud se exponen a través de HttpContext.RequestServices.

El marco crea un ámbito por solicitud, y RequestServices expone el proveedor de servicios con ámbito. Todos los servicios con ámbito son válidos mientras la solicitud está activa.

Nota

Se recomienda que solicite las dependencias como parámetros del constructor en vez de resolver los servicios desde RequestServices. La solicitud de dependencias como parámetros de constructor produce clases que son más fáciles de probar.

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. Tenga en cuenta que las clases del modelo de página de Razor Pages y las clases del controlador MVC deben centrarse en aspectos de la interfaz de usuario.

Eliminación de servicios

El contenedor llama a Dispose para los tipos IDisposable que crea. 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: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

La consola de depuración muestra la siguiente salida después de cada actualización de la página de índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Servicios no creados por el contenedor de servicios

Observe el código siguiente:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

En el código anterior:

  • El contenedor de servicios no crea las instancias de servicio.
  • 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

Consulte Instrucciones de IDisposable para instancias transitorias y compartidas en la Inserción de dependencias en .NET.

Reemplazo del contenedor de servicios predeterminado

Consulte Reemplazo de contenedores de servicio predeterminados en Inserción de dependencias en .NET.

Recomendaciones

Consulte Recomendaciones en Inserción de dependencias en .NET.

  • 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:

    Incorrecto:

    Código incorrecto

    Correcto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 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 el acceso estático a HttpContext (por ejemplo, IHttpContextAccessor.HttpContext).

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.

Orchard Core es un marco de trabajo de la aplicación para compilar aplicaciones modulares y multiinquilino en ASP.NET Core. Para obtener más información, vea la documentación de Orchard Core.

Consulte los ejemplos de Orchard Core para obtener ejemplos sobre cómo compilar aplicaciones modulares y multiinquilino mediante el uso exclusivo del marco de Orchard Core, sin ninguna de las características específicas de CMS.

Servicios proporcionados por el marco de trabajo

Program.cs registra los servicios que la aplicación usa, incluidas las características de plataforma, como Entity Framework Core y ASP.NET Core MVC. Inicialmente, el valor IServiceCollection proporcionado a Program.cs tiene los servicios definidos por el marco en función de cómo se configurara el host. En el caso de las aplicaciones basadas en las plantillas de ASP.NET Core, el marco de trabajo registra más de 250 servicios.

En la tabla siguiente se ilustra una pequeña muestra de estos servicios registrados por el marco:

Tipo de servicio Período de duración
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitorio
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitorio
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitorio
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitorio
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionales

De Kirk Larkin, Steve Smith y Brandon Dahler

ASP.NET Core admite el patrón de diseño de software de inserción de dependencias (DI), que es una técnica para conseguir la inversión de control (IoC) entre clases y sus dependencias.

Para más información específica de la inserción de dependencias en controladores de MVC, consulte Inserción de dependencias en controladores en ASP.NET Core.

Para obtener información sobre el uso de la inserción de dependencias en aplicaciones que no son aplicaciones web, consulte Inserción de dependencias en .NET.

Para más información sobre la inserción de dependencias de opciones, consulte Patron de opciones en ASP.NET Core.

En este tema se proporciona información sobre la inyección de dependencias en ASP.NET Core. La documentación principal sobre el uso de la inserción de dependencias se incluye en Inserción de dependencias en .NET.

Vea o descargue el código de ejemplo (cómo descargarlo)

Información general sobre la inserción de dependencias

Una dependencia es un objeto del que depende otro objeto. Examine la clase MyDependency siguiente con un método WriteMessage del que dependen otras clases:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Una clase puede crear una instancia de la clase MyDependency para usar su método WriteMessage. En el ejemplo siguiente, la clase MyDependency es una dependencia de la clase IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

La clase crea y depende directamente de la clase MyDependency. Las dependencias de código, como en el ejemplo anterior, son problemáticas y deben evitarse por las razones siguientes:

  • Para reemplazar MyDependency por una implementación diferente, se debe modificar la clase IndexModel.
  • Si MyDependency tiene dependencias, deben configurarse según la clase IndexModel. En un proyecto grande con varias clases que dependen de MyDependency, el código de configuración se dispersa por la aplicación.
  • Esta implementación es difícil para realizar pruebas unitarias.

La inserción de dependencias aborda estos problemas mediante:

  • Uso de una interfaz o clase base para abstraer la implementación de dependencias.
  • Registro de la dependencia en un contenedor de servicios. ASP.NET Core proporciona un contenedor de servicios integrado, IServiceProvider. Por lo general, los servicios se registran en el archivo Program.cs de la aplicación.
  • Inserción del servicio en el constructor de la clase en la que se usa. El marco de trabajo asume la responsabilidad de crear una instancia de la dependencia y de desecharla cuando ya no es necesaria.

En la aplicación de ejemplo, la interfaz IMyDependency define el método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Esta interfaz se implementa mediante un tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

La aplicación de ejemplo registra el servicio IMyDependency con el tipo concreto MyDependency. El método AddScoped registra el servicio mediante una duración con ámbito, definida como la duración de una única solicitud. Las duraciones del servicio se describen más adelante en este tema.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

En la aplicación de ejemplo, el servicio IMyDependency se solicita y usa para llamar al método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Usando el patrón de DI, el controlador o la página de Razor:

  • No usa el tipo concreto MyDependency, solo la interfaz IMyDependency que implementa. Esto facilita el cambio de la implementación sin modificar el controlador ni la página de Razor.
  • No crea una instancia de MyDependency, la crea el contenedor de DI.

La implementación de la interfaz de IMyDependency se puede mejorar mediante el uso de la API de registro integrada:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

El objeto Program.cs actualizado registra la nueva implementación de IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 depende de ILogger<TCategoryName>, el que solicita en el constructor. ILogger<TCategoryName> es un servicio proporcionado por el marco de trabajo.

No es raro usar la inserción de dependencias de forma encadenada. Cada dependencia solicitada a su vez solicita sus propias dependencias. El contenedor resuelve las dependencias del gráfico y devuelve el servicio totalmente resuelto. El conjunto colectivo de dependencias que deben resolverse suele denominarse árbol de dependencias, gráfico de dependencias o gráfico de objetos.

El contenedor resuelve ILogger<TCategoryName> aprovechando las ventajas de los tipos abiertos (genéricos), lo que elimina la necesidad de registrar todos los tipos construidos (genéricos).

En la terminología de la inserción de dependencias, un servicio:

  • Por lo general, es un objeto que proporciona un servicio a otros objetos, como el servicio IMyDependency.
  • No está relacionado con un servicio web, aunque el servicio puede utilizar un servicio web.

El marco de trabajo proporciona un sistema de registro sólido. Las implementaciones de IMyDependency que se muestran en los ejemplos anteriores se escribieron para mostrar la inserción de DI básica, no para implementar el registro. La mayoría de las aplicaciones no deberían tener que escribir registradores. En el código siguiente se muestra cómo usar el registro predeterminado, que no requiere que los servicios se registren:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Con el código anterior, no es necesario actualizar Program.cs porque el registro se proporciona a través del marco de trabajo.

Registro de grupos de servicios con métodos de extensión

El marco ASP.NET Core usa una convención para registrar un grupo de servicios relacionados. La convención es usar un único método de extensión de Add{GROUP_NAME} para registrar todos los servicios requeridos por una característica de marco. Por ejemplo, el método de extensión AddControllers registra los servicios necesarios para los controladores MVC.

El código siguiente lo genera la plantilla de Razor Pages con cuentas de usuario individuales y muestra cómo agregar servicios adicionales al contenedor mediante los métodos de extensión AddDbContext y AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Tenga en cuenta el siguiente método que registra los servicios y configura las opciones:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Los grupos de registros relacionados pueden moverse a un método de extensión para registrar los servicios. Por ejemplo, los servicios de configuración se agregan a la siguiente clase:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Los servicios restantes se registran en una clase similar. El siguiente código usa los nuevos métodos de extensión para registrar los servicios:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Nota: Cada método de extensión services.Add{GROUP_NAME} agrega servicios y potencialmente los configura. Por ejemplo, AddControllersWithViews agrega los controladores MVC de servicios con las vistas que se requieren y AddRazorPages agrega los servicios que Razor Pages requiere.

Duraciones de servicios

Consulte Duración del servicio en Inserción de dependencias en .NET.

Para usar servicios con ámbito en middleware, use uno de los enfoques siguientes:

  • Inserte el servicio en el método Invoke o InvokeAsync del middleware. El uso de la inserción de constructores genera una excepción en tiempo de ejecución porque obliga al servicio con ámbito a comportarse como un singleton. El ejemplo que aparece en la sección Opciones de registro y duración muestra el enfoque InvokeAsync.
  • Use middleware basado en Factory. El middleware registrado con este enfoque se activa por solicitud de cliente (conexión), lo que permite insertar servicios con ámbito en el constructor del middleware.

Para obtener más información, vea Escritura de middleware de ASP.NET Core personalizado.

Métodos de registro del servicio

Consulte Métodos de registro del servicio en Inserción de dependencias en .NET.

Es habitual usar varias implementaciones al utilizar tipos de simulación para las pruebas.

El registro de un servicio con un solo tipo de implementación es equivalente al registro de ese servicio con la misma implementación y el mismo tipo de servicio. Por eso no se pueden registrar varias implementaciones de un servicio mediante los métodos que no toman un tipo de servicio explícito. Estos métodos pueden registrar varias instancias de un servicio, pero todos tienen el mismo tipo de implementación.

Cualquiera de los métodos de registro de servicio anteriores se puede usar para registrar varias instancias de servicio del mismo tipo de servicio. En el ejemplo siguiente se llama a AddSingleton dos veces con IMyDependency como tipo de servicio. La segunda llamada a AddSingleton invalida la anterior cuando se resuelve como IMyDependency, y se agrega a la anterior cuando varios servicios se resuelven mediante IEnumerable<IMyDependency>. Los servicios aparecen en el orden en que se han registrado al resolverse mediante IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Comportamiento de inserción de constructor

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

Contextos de Entity Framework

De manera predeterminada, los contextos de Entity Framework se agregan al contenedor de servicios mediante la duración con ámbito porque las operaciones de base de datos de aplicación web se suelen limitar a la solicitud de cliente. Para usar una duración distinta, especifíquela mediante el uso de una sobrecarga de AddDbContext. En los servicios de una duración determinada no se debe usar un contexto de base de datos con una duración más corta que la del servicio.

Opciones de registro y duración

Para mostrar la diferencia entre las duraciones del servicio y sus opciones de registro, considere las interfaces siguientes que representan una tarea como una operación con un identificador, OperationId. Según cómo esté configurada la duración de un servicio de operaciones para las interfaces siguientes, el contenedor proporciona las mismas instancias del servicio u otras distintas cuando así lo solicita la clase:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

La clase Operation siguiente implementa todas las interfaces anteriores. El constructor Operation genera un GUID y almacena los 4 últimos caracteres en la propiedad OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

El siguiente código crea varios registros de la clase Operation de acuerdo con las duraciones mencionadas:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

La aplicación de ejemplo muestra las duraciones de los objetos dentro y entre las solicitudes. IndexModel y el middleware solicitan cada clase del tipo IOperation y registran el valor de OperationId de cada una:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

De manera similar al IndexModel, el middleware resuelve los mismos servicios:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Los servicios con ámbito y transitorios se deben resolver en el método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

La salida del registrador muestra:

  • Los objetos Transient siempre son diferentes. El valor OperationId transitorio es distinto en el IndexModel y en el middleware.
  • Los objetos con ámbito son iguales para una determinada solicitud, pero varían entre solicitudes.
  • Los objetos singleton son los mismos para cada solicitud.

Para reducir la salida del registro, establezca "Logging:LogLevel:Microsoft:Error" en el archivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Resolución de un servicio al iniciar la aplicación

El código siguiente muestra cómo resolver un servicio con ámbito durante un tiempo limitado cuando se inicia la aplicación:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validación del ámbito

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

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

Servicios de solicitud

Los servicios y sus dependencias dentro de ASP.NET Core solicitud se exponen a través de HttpContext.RequestServices.

El marco crea un ámbito por solicitud, y RequestServices expone el proveedor de servicios con ámbito. Todos los servicios con ámbito son válidos mientras la solicitud está activa.

Nota

Se recomienda que solicite las dependencias como parámetros del constructor en vez de resolver los servicios desde RequestServices. La solicitud de dependencias como parámetros de constructor produce clases que son más fáciles de probar.

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. Tenga en cuenta que las clases del modelo de página de Razor Pages y las clases del controlador MVC deben centrarse en aspectos de la interfaz de usuario.

Eliminación de servicios

El contenedor llama a Dispose para los tipos IDisposable que crea. 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: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

La consola de depuración muestra la siguiente salida después de cada actualización de la página de índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Servicios no creados por el contenedor de servicios

Observe el código siguiente:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

En el código anterior:

  • El contenedor de servicios no crea las instancias de servicio.
  • 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

Consulte Instrucciones de IDisposable para instancias transitorias y compartidas en la Inserción de dependencias en .NET.

Reemplazo del contenedor de servicios predeterminado

Consulte Reemplazo de contenedores de servicio predeterminados en Inserción de dependencias en .NET.

Recomendaciones

Consulte Recomendaciones en Inserción de dependencias en .NET.

  • 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:

    Incorrecto:

    Código incorrecto

    Correcto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 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 el acceso estático a HttpContext (por ejemplo, IHttpContextAccessor.HttpContext).

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.

Orchard Core es un marco de trabajo de la aplicación para compilar aplicaciones modulares y multiinquilino en ASP.NET Core. Para obtener más información, vea la documentación de Orchard Core.

Consulte los ejemplos de Orchard Core para obtener ejemplos sobre cómo compilar aplicaciones modulares y multiinquilino mediante el uso exclusivo del marco de Orchard Core, sin ninguna de las características específicas de CMS.

Servicios proporcionados por el marco de trabajo

Program.cs registra los servicios que la aplicación usa, incluidas las características de plataforma, como Entity Framework Core y ASP.NET Core MVC. Inicialmente, el valor IServiceCollection proporcionado a Program.cs tiene los servicios definidos por el marco en función de cómo se configurara el host. En el caso de las aplicaciones basadas en las plantillas de ASP.NET Core, el marco de trabajo registra más de 250 servicios.

En la tabla siguiente se ilustra una pequeña muestra de estos servicios registrados por el marco:

Tipo de servicio Período de duración
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitorio
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitorio
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitorio
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitorio
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionales

Por Kirk Larkin, Steve Smith, Scott Addie y Brandon Dahler

ASP.NET Core admite el patrón de diseño de software de inserción de dependencias (DI), que es una técnica para conseguir la inversión de control (IoC) entre clases y sus dependencias.

Para más información específica de la inserción de dependencias en controladores de MVC, consulte Inserción de dependencias en controladores en ASP.NET Core.

Para obtener información sobre el uso de la inserción de dependencias en aplicaciones que no son aplicaciones web, consulte Inserción de dependencias en .NET.

Para más información sobre la inserción de dependencias de opciones, consulte Patron de opciones en ASP.NET Core.

En este tema se proporciona información sobre la inyección de dependencias en ASP.NET Core. La documentación principal sobre el uso de la inserción de dependencias se incluye en Inserción de dependencias en .NET.

Vea o descargue el código de ejemplo (cómo descargarlo)

Información general sobre la inserción de dependencias

Una dependencia es un objeto del que depende otro objeto. Examine la clase MyDependency siguiente con un método WriteMessage del que dependen otras clases:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Una clase puede crear una instancia de la clase MyDependency para usar su método WriteMessage. En el ejemplo siguiente, la clase MyDependency es una dependencia de la clase IndexModel:

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

La clase crea y depende directamente de la clase MyDependency. Las dependencias de código, como en el ejemplo anterior, son problemáticas y deben evitarse por las razones siguientes:

  • Para reemplazar MyDependency por una implementación diferente, se debe modificar la clase IndexModel.
  • Si MyDependency tiene dependencias, deben configurarse según la clase IndexModel. En un proyecto grande con varias clases que dependen de MyDependency, 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 MyDependency como boceto o código auxiliar, que no es posible con este enfoque.

La inserción de dependencias aborda estos problemas mediante:

  • Uso de una interfaz o clase base para abstraer la implementación de dependencias.
  • Registro de la dependencia en un contenedor de servicios. ASP.NET Core proporciona un contenedor de servicios integrado, IServiceProvider. Por lo general, los servicios se registran en el método Startup.ConfigureServices de la aplicación.
  • Inserción del servicio en el constructor de la clase en la que se usa. El marco de trabajo asume la responsabilidad de crear una instancia de la dependencia y de desecharla cuando ya no es necesaria.

En la aplicación de ejemplo, la interfaz IMyDependency define el método WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Esta interfaz se implementa mediante un tipo concreto, MyDependency:

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

La aplicación de ejemplo registra el servicio IMyDependency con el tipo concreto MyDependency. El método AddScoped registra el servicio mediante una duración con ámbito, definida como la duración de una única solicitud. Las duraciones del servicio se describen más adelante en este tema.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

En la aplicación de ejemplo, el servicio IMyDependency se solicita y usa para llamar al método WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Mediante el uso del patrón de DI, el controlador:

  • No usa el tipo concreto MyDependency, solo la interfaz IMyDependency que implementa. Esto facilita el cambio de la implementación que el controlador utiliza sin modificar el controlador.
  • No crea una instancia de MyDependency, la crea el contenedor de DI.

La implementación de la interfaz de IMyDependency se puede mejorar mediante el uso de la API de registro integrada:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

El método ConfigureServices actualizado registra la nueva implementación de IMyDependency:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 depende de ILogger<TCategoryName>, el que solicita en el constructor. ILogger<TCategoryName> es un servicio proporcionado por el marco de trabajo.

No es raro usar la inserción de dependencias de forma encadenada. Cada dependencia solicitada a su vez solicita sus propias dependencias. El contenedor resuelve las dependencias del gráfico y devuelve el servicio totalmente resuelto. El conjunto colectivo de dependencias que deben resolverse suele denominarse árbol de dependencias, gráfico de dependencias o gráfico de objetos.

El contenedor resuelve ILogger<TCategoryName> aprovechando las ventajas de los tipos abiertos (genéricos), lo que elimina la necesidad de registrar todos los tipos construidos (genéricos).

En la terminología de la inserción de dependencias, un servicio:

  • Por lo general, es un objeto que proporciona un servicio a otros objetos, como el servicio IMyDependency.
  • No está relacionado con un servicio web, aunque el servicio puede utilizar un servicio web.

El marco de trabajo proporciona un sistema de registro sólido. Las implementaciones de IMyDependency que se muestran en los ejemplos anteriores se escribieron para mostrar la inserción de DI básica, no para implementar el registro. La mayoría de las aplicaciones no deberían tener que escribir registradores. En el código siguiente se muestra cómo usar el registro predeterminado, que no requiere que los servicios se registren en ConfigureServices:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Con el código anterior, no es necesario actualizar ConfigureServices porque el registro se proporciona a través del marco de trabajo.

Servicios insertados en Startup

Los servicios se pueden insertar en el constructor Startup y en el método Startup.Configure.

Solo se pueden insertar los servicios siguientes en el constructor Startup cuando se usa el host genérico (IHostBuilder):

Cualquier servicio registrado con el contenedor de DI se puede insertar en el método Startup.Configure:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

Para más información, consulte Inicio de aplicaciones en ASP.NET Core y Configuración del acceso en el inicio.

Registro de grupos de servicios con métodos de extensión

El marco ASP.NET Core usa una convención para registrar un grupo de servicios relacionados. La convención es usar un único método de extensión de Add{GROUP_NAME} para registrar todos los servicios requeridos por una característica de marco. Por ejemplo, el método de extensión AddControllers registra los servicios necesarios para los controladores MVC.

El código siguiente lo genera la plantilla de Razor Pages con cuentas de usuario individuales y muestra cómo agregar servicios adicionales al contenedor mediante los métodos de extensión AddDbContext y AddDefaultIdentity:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Tenga en cuenta el siguiente método ConfigureServices, que registra los servicios y configura las opciones:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

Los grupos de registros relacionados pueden moverse a un método de extensión para registrar los servicios. Por ejemplo, los servicios de configuración se agregan a la siguiente clase:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Los servicios restantes se registran en una clase similar. El siguiente método ConfigureServices usa los nuevos métodos de extensión para registrar los servicios:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

Nota: Cada método de extensión services.Add{GROUP_NAME} agrega servicios y potencialmente los configura. Por ejemplo, AddControllersWithViews agrega los controladores MVC de servicios con las vistas que se requieren y AddRazorPages agrega los servicios que Razor Pages requiere. Se recomienda que las aplicaciones sigan la convención de nomenclatura de crear métodos de extensión en el espacio de nombres Microsoft.Extensions.DependencyInjection. Creación de métodos de extensión en el espacio de nombres Microsoft.Extensions.DependencyInjection:

  • Encapsula grupos de registros de servicio.
  • Proporciona acceso práctico de IntelliSense al servicio.

Duraciones de servicios

Consulte Duración del servicio en Inserción de dependencias en .NET.

Para usar servicios con ámbito en middleware, use uno de los enfoques siguientes:

  • Inserte el servicio en el método Invoke o InvokeAsync del middleware. El uso de la inserción de constructores genera una excepción en tiempo de ejecución porque obliga al servicio con ámbito a comportarse como un singleton. El ejemplo que aparece en la sección Opciones de registro y duración muestra el enfoque InvokeAsync.
  • Use middleware basado en Factory. El middleware registrado mediante este enfoque se activa por solicitud de cliente (conexión), lo que permite que los servicios con ámbito se inserten en el método InvokeAsync del middleware.

Para obtener más información, vea Escritura de middleware de ASP.NET Core personalizado.

Métodos de registro del servicio

Consulte Métodos de registro del servicio en Inserción de dependencias en .NET.

Es habitual usar varias implementaciones al utilizar tipos de simulación para las pruebas.

El registro de un servicio con un solo tipo de implementación es equivalente al registro de ese servicio con la misma implementación y el mismo tipo de servicio. Por eso no se pueden registrar varias implementaciones de un servicio mediante los métodos que no toman un tipo de servicio explícito. Estos métodos pueden registrar varias instancias de un servicio, pero todos tienen el mismo tipo de implementación.

Cualquiera de los métodos de registro de servicio anteriores se puede usar para registrar varias instancias de servicio del mismo tipo de servicio. En el ejemplo siguiente se llama a AddSingleton dos veces con IMyDependency como tipo de servicio. La segunda llamada a AddSingleton invalida la anterior cuando se resuelve como IMyDependency, y se agrega a la anterior cuando varios servicios se resuelven mediante IEnumerable<IMyDependency>. Los servicios aparecen en el orden en que se han registrado al resolverse mediante IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Comportamiento de inserción de constructor

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

Contextos de Entity Framework

De manera predeterminada, los contextos de Entity Framework se agregan al contenedor de servicios mediante la duración con ámbito porque las operaciones de base de datos de aplicación web se suelen limitar a la solicitud de cliente. Para usar una duración distinta, especifíquela mediante el uso de una sobrecarga de AddDbContext. En los servicios de una duración determinada no se debe usar un contexto de base de datos con una duración más corta que la del servicio.

Opciones de registro y duración

Para mostrar la diferencia entre las duraciones del servicio y sus opciones de registro, considere las interfaces siguientes que representan una tarea como una operación con un identificador, OperationId. Según cómo esté configurada la duración de un servicio de operaciones para las interfaces siguientes, el contenedor proporciona las mismas instancias del servicio u otras distintas cuando así lo solicita la clase:

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

La clase Operation siguiente implementa todas las interfaces anteriores. El constructor Operation genera un GUID y almacena los 4 últimos caracteres en la propiedad OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

El método Startup.ConfigureServices crea varios registros de la clase Operation de acuerdo con las duraciones mencionadas:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

La aplicación de ejemplo muestra las duraciones de los objetos dentro y entre las solicitudes. IndexModel y el middleware solicitan cada clase del tipo IOperation y registran el valor de OperationId de cada una:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

De manera similar al IndexModel, el middleware resuelve los mismos servicios:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Los servicios con ámbito se deben resolver en el método InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + _transientOperation.OperationId);
    _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

La salida del registrador muestra:

  • Los objetos Transient siempre son diferentes. El valor OperationId transitorio es distinto en el IndexModel y en el middleware.
  • Los objetos con ámbito son iguales para una determinada solicitud, pero varían entre solicitudes.
  • Los objetos singleton son los mismos para cada solicitud.

Para reducir la salida del registro, establezca "Logging:LogLevel:Microsoft:Error" en el archivo appsettings.Development.json:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Llamada a servicios desde main

Cree un elemento IServiceScope con IServiceScopeFactory.CreateScope para resolver un servicio con ámbito dentro del ámbito de la aplicación. Este método resulta útil para tener acceso a un servicio con ámbito durante el inicio para realizar tareas de inicialización.

En el ejemplo siguiente se muestra cómo acceder al servicio IMyDependency con ámbito y llamar a su método WriteMessage en Program.Main:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Validación del ámbito

Consulte Comportamiento de inserción de constructor en Inserción de dependencias en .NET.

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

Servicios de solicitud

Los servicios y sus dependencias dentro de ASP.NET Core solicitud se exponen a través de HttpContext.RequestServices.

El marco crea un ámbito por solicitud, y RequestServices expone el proveedor de servicios con ámbito. Todos los servicios con ámbito son válidos mientras la solicitud está activa.

Nota

Se recomienda que solicite las dependencias como parámetros del constructor en vez de resolver los servicios desde RequestServices. La solicitud de dependencias como parámetros de constructor produce clases que son más fáciles de probar.

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. Tenga en cuenta que las clases del modelo de página de Razor Pages y las clases del controlador MVC deben centrarse en aspectos de la interfaz de usuario.

Eliminación de servicios

El contenedor llama a Dispose para los tipos IDisposable que crea. 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:

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

La consola de depuración muestra la siguiente salida después de cada actualización de la página de índice:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = My Key from config
Service1.Dispose

Servicios no creados por el contenedor de servicios

Observe el código siguiente:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

En el código anterior:

  • El contenedor de servicios no crea las instancias de servicio.
  • 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

Consulte Instrucciones de IDisposable para instancias transitorias y compartidas en la Inserción de dependencias en .NET.

Reemplazo del contenedor de servicios predeterminado

Consulte Reemplazo de contenedores de servicio predeterminados en Inserción de dependencias en .NET.

Recomendaciones

Consulte Recomendaciones en Inserción de dependencias en .NET.

  • 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:

    Incorrecto:

    Código incorrecto

    Correcto:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 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 el acceso estático a HttpContext (por ejemplo, IHttpContextAccessor.HttpContext).

  • Evite las llamadas a BuildServiceProvider en ConfigureServices. La llamada a BuildServiceProvider suele ocurrir cuando el desarrollador desea resolver un servicio en ConfigureServices. Por ejemplo, considere el caso donde LoginPath se carga desde la configuración. Evite el enfoque siguiente:

    código incorrecto llamando a BuildServiceProvider

    En la imagen anterior, al seleccionar la línea verde ondulada debajo de services.BuildServiceProvider se muestra la siguiente advertencia ASP0000:

    ASP0000 Al llamar a "BuildServiceProvider" desde el código de aplicación, se crea una copia adicional de los servicios singleton. Considere alternativas como los servicios de inserción de dependencias como parámetros para "Configurar".

    La llamada a BuildServiceProvider crea un segundo contenedor que puede crear singletons rasgados y producir referencias a gráficos de objetos en varios contenedores.

    Una manera correcta de obtener LoginPath es usar la compatibilidad integrada del patrón de opciones para DI:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • El contenedor captura a los servicios transitorios descartables para su eliminación. Esto puede convertirse en una pérdida 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.

Orchard Core es un marco de trabajo de la aplicación para compilar aplicaciones modulares y multiinquilino en ASP.NET Core. Para obtener más información, vea la documentación de Orchard Core.

Consulte los ejemplos de Orchard Core para obtener ejemplos sobre cómo compilar aplicaciones modulares y multiinquilino mediante el uso exclusivo del marco de Orchard Core, sin ninguna de las características específicas de CMS.

Servicios proporcionados por el marco de trabajo

El método Startup.ConfigureServices registra los servicios que la aplicación usa, incluidas las características de plataforma, como Entity Framework Core y ASP.NET Core MVC. Inicialmente, el valor IServiceCollection proporcionado a ConfigureServices tiene los servicios definidos por el marco en función de cómo se configurara el host. En el caso de las aplicaciones basadas en las plantillas de ASP.NET Core, el marco de trabajo registra más de 250 servicios.

En la tabla siguiente se ilustra una pequeña muestra de estos servicios registrados por el marco:

Tipo de servicio Período de duración
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transitorio
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transitorio
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Transitorio
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transitorio
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Recursos adicionales