No se puede resolver el servicio de ámbito del proveedor raíz .Net Core 2

83

Cuando intento ejecutar mi aplicación, aparece el error

InvalidOperationException: Cannot resolve 'API.Domain.Data.Repositories.IEmailRepository' from root provider because it requires scoped service 'API.Domain.Data.EmailRouterContext'.

Lo que es extraño es que este EmailRepository y la interfaz están configurados exactamente de la misma manera que todos mis otros repositorios, pero no se arroja ningún error para ellos. El error solo ocurre si trato de usar la aplicación.UseEmailingExceptionHandling (); línea. Aquí hay algunos de mis archivos Startup.cs.

public class Startup
{
    public IConfiguration Configuration { get; protected set; }
    private APIEnvironment _environment { get; set; }

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        Configuration = configuration;

        _environment = APIEnvironment.Development;
        if (env.IsProduction()) _environment = APIEnvironment.Production;
        if (env.IsStaging()) _environment = APIEnvironment.Staging;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var dataConnect = new DataConnect(_environment);

        services.AddDbContext<GeneralInfoContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.GeneralInfo)));
        services.AddDbContext<EmailRouterContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.EmailRouter)));

        services.AddWebEncoders();
        services.AddMvc();

        services.AddScoped<IGenInfoNoteRepository, GenInfoNoteRepository>();
        services.AddScoped<IEventLogRepository, EventLogRepository>();
        services.AddScoped<IStateRepository, StateRepository>();
        services.AddScoped<IEmailRepository, EmailRepository>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();

        app.UseAuthentication();

        app.UseStatusCodePages();
        app.UseEmailingExceptionHandling();

        app.UseMvcWithDefaultRoute();
    }
}

Aquí está el repositorio de correo electrónico

public interface IEmailRepository
{
    void SendEmail(Email email);
}

public class EmailRepository : IEmailRepository, IDisposable
{
    private bool disposed;
    private readonly EmailRouterContext edc;

    public EmailRepository(EmailRouterContext emailRouterContext)
    {
        edc = emailRouterContext;
    }

    public void SendEmail(Email email)
    {
        edc.EmailMessages.Add(new EmailMessages
        {
            DateAdded = DateTime.Now,
            FromAddress = email.FromAddress,
            MailFormat = email.Format,
            MessageBody = email.Body,
            SubjectLine = email.Subject,
            ToAddress = email.ToAddress
        });
        edc.SaveChanges();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
                edc.Dispose();
            disposed = true;
        }
    }
}

Y finalmente el middleware de manejo de excepciones

public class ExceptionHandlingMiddleware
{
    private const string ErrorEmailAddress = "[email protected]";
    private readonly IEmailRepository _emailRepository;

    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next, IEmailRepository emailRepository)
    {
        _next = next;
        _emailRepository = emailRepository;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex, _emailRepository);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception,
        IEmailRepository emailRepository)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        var email = new Email
        {
            Body = exception.Message,
            FromAddress = ErrorEmailAddress,
            Subject = "API Error",
            ToAddress = ErrorEmailAddress
        };

        emailRepository.SendEmail(email);

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int) code;
        return context.Response.WriteAsync("An error occured.");
    }
}

public static class AppErrorHandlingExtensions
{
    public static IApplicationBuilder UseEmailingExceptionHandling(this IApplicationBuilder app)
    {
        if (app == null)
            throw new ArgumentNullException(nameof(app));
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

Actualización: encontré este enlace https://github.com/aspnet/DependencyInjection/issues/578 que me llevó a cambiar el método BuildWebHost de mi archivo Program.cs de este

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();
}

a esto

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseDefaultServiceProvider(options =>
            options.ValidateScopes = false)
        .Build();
}

No sé qué está pasando exactamente, pero parece funcionar ahora.

Geoff Swartz
fuente
4
Lo que está sucediendo allí es que el anidamiento del alcance no se está validando; como en, no está comprobando, durante el tiempo de ejecución, si tiene un anidamiento inadecuado del nivel de alcance. Aparentemente, esto estaba desactivado de forma predeterminada en 1.1. Una vez que llegó 2.0, lo activaron de forma predeterminada.
Robert Burke
Para cualquiera que intente apagar ValidateScopes, lea esto stackoverflow.com/a/50198738/1027250
Yorro

Respuestas:

174

Registraste el IEmailRepositorycomo un servicio de ámbito, en la Startupclase. Esto significa que no puede inyectarlo como un parámetro de constructor en Middlewareporque solo los Singletonservicios se pueden resolver mediante la inyección de constructor en Middleware. Debes mover la dependencia al Invokemétodo de esta manera:

public ExceptionHandlingMiddleware(RequestDelegate next)
{
    _next = next;
}

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{
    try
    {
        await _next.Invoke(context);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex, emailRepository);
    }
}
user1336
fuente
12
¡Guauu! Nunca supe que podrías inyectar en métodos, ¿es esto solo para middleware o puedo usar este truco en mis propios métodos?
Fergal Moran
¿Qué pasa con IMiddleware que está registrado como ámbito? Sé con certeza que obtengo una nueva instancia de middleware, pero todavía no puedo inyectarle un servicio de ámbito.
Botis
2
@FergalMoran Desafortunadamente, este "truco" es un comportamiento especial del Invokemétodo del middleware . Sin embargo, puede lograr algo similar a través de autofac IoC lib y la inyección de propiedades. Consulte ¿ Inyección de dependencia de ASP.NET Core MVC a través de la propiedad o el método de establecimiento? .
B12Toaster
4
La inyección no es mágica. Hay un motor detrás de escena que realmente llama al contenedor de dependencia para generar instancias y pasarlas como parámetros a constructores o métodos. Este motor particular busca métodos llamados "Invoke" con un primer argumento de HttpContext y luego crea instancias para el resto de los parámetros.
Thanasis Ioannidis
86

Otra forma de obtener la instancia de dependencia de ámbito es inyectar el proveedor de servicios ( IServiceProvider) en el constructor de middleware, crear scopeen el Invokemétodo y luego obtener el servicio requerido del ámbito:

using (var scope = _serviceProvider.CreateScope()) {
    var _emailRepository = scope.ServiceProvider.GetRequiredService<IEmailRepository>();

    //do your stuff....
}

Consulte Resolver servicios en un cuerpo de método en asp.net, trucos de consejos de mejores prácticas de inyección de dependencia principal para obtener más detalles.

Riddik
fuente
5
¡Muy útil, gracias! Para cualquiera que intente acceder a EF Contexts en middleware, este es el camino a seguir, ya que tienen un alcance predeterminado.
ntziolis
stackoverflow.com/a/49886317/502537 hace esto de forma más directa
RickAndMSFT
Al principio no pensé que esto funcionara, pero luego me di cuenta de que lo estás haciendo en scope.ServiceProviderlugar de hacerlo _serviceProvideren la segunda línea. Gracias por esto.
adam0101
_serviceProvider.CreateScope (). ServiceProvider funciona mejor para mí
XLR8
Creo que sería mejor usarlo IServiceScopeFactorypara este propósito
Francesco DM
27

El middleware siempre es un singleton, por lo que no puede tener dependencias de ámbito como dependencias de constructor en el constructor de su middleware.

El middleware admite la inyección de métodos en el método Invoke, por lo que puede agregar IEmailRepository emailRepository como parámetro a ese método y se inyectará allí y estará bien según el alcance.

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{

    ....
}
Joe Audette
fuente
Estaba en una situación similar, luego agregué el servicio usando AddTransient y pude resolver la dependencia. Pensé que no funcionaría ya que el middleware es singleton. un poco extraño ..
Sateesh Pagolu
1
Creo que una dependencia transitoria debería eliminarse manualmente, a diferencia del ámbito, que se eliminará automáticamente al final de la solicitud web donde se creó por primera vez. Tal vez un desechable transitorio dentro de una dependencia con alcance se eliminaría y el objeto externo se eliminaría. Aún así, no estoy seguro de que una dependencia transitoria dentro de un singleton o un objeto con una vida útil más larga que transitoria sea una buena idea, creo que evitaría eso.
Joe Audette
2
Aunque puede inyectar una dependencia de ámbito transitorio a través del constructor en este caso, no se creará una instancia como cree. Solo sucederá una vez, cuando se construya el Singleton.
Jonathan
1
Ha mencionado que el middleware siempre es un singleton, pero no es cierto. Es posible crear un middleware como un middleware basado en fábrica y usarlo como un middleware de ámbito.
Harun Diluka Heshan
Parece que el middleware basado en fábrica se introdujo en asp.netcore 2.2 y la documentación se creó en 2019. Entonces, mi respuesta fue cierta cuando la publiqué , hasta donde yo sé. El middleware basado en fábrica parece una buena solución hoy en día.
Joe Audette
4

Your middlewarey the servicetienen que ser compatibles entre sí para poder inyectar la servicevía constructorde su middleware. Aquí, tu middlewarese ha creado como, lo convention-based middlewareque significa que actúa como un singleton servicey tú has creado tu servicio como scoped-service. Por lo tanto, no puede inyectar a scoped-serviceen el constructor de a singleton-serviceporque obliga scoped-servicea que actúe como singletonuno. Sin embargo, estas son sus opciones.

  1. Inyecta tu servicio como parámetro al InvokeAsyncmétodo.
  2. Haga que su servicio sea único, si es posible.
  3. Transforma tu middlewareen factory-baseduno.

A Factory-based middlewarees capaz de actuar como scoped-service. Entonces, puede inyectar otro a scoped-servicetravés del constructor de ese middleware. A continuación, le he mostrado cómo crear un factory-basedmiddleware.

Esto es solo para demostración. Entonces, he eliminado todo el otro código.

public class Startup
{
    public Startup()
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<TestMiddleware>();
        services.AddScoped<TestService>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<TestMiddleware>();
    }
}

El TestMiddleware:

public class TestMiddleware : IMiddleware
{
    public TestMiddleware(TestService testService)
    {
    }

    public Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        return next.Invoke(context);
    }
}

El TestService:

public class TestService
{
}
Harun Diluka Heshan
fuente