ChatGPT解决这个技术问题 Extra ChatGPT

Cannot resolve scoped service from root provider .Net Core 2

When I try to run my app I get the error

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

What's odd is that this EmailRepository and interface is set up exactly the same as far as I can tell as all of my other repositories yet no error is thrown for them. The error only occurs if I try to use the app.UseEmailingExceptionHandling(); line. Here's some of my Startup.cs file.

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();
    }
}

Here is the EmailRepository

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;
        }
    }
}

And finally the exception handling middleware

public class ExceptionHandlingMiddleware
{
    private const string ErrorEmailAddress = "errors@ourdomain.com";
    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>();
    }
}

Update: I found this link https://github.com/aspnet/DependencyInjection/issues/578 which led me to change my Program.cs file's BuildWebHost method from this

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

to this

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

I don't know what exactly is going on but it seems to work now.

What's happening there, is that the scope nesting isn't being validated; as in, it isn't checking, during runtime, if you have improper nesting of scope level. Apparently, this was turned off by default in 1.1. Once 2.0 came along, they turned it on by default.
To anyone attempting to turn off the ValidateScopes, please read this stackoverflow.com/a/50198738/1027250

T
TanvirArjel

You registered the IEmailRepository as a scoped service, in the Startup class. This means that you can not inject it as a constructor parameter in Middleware because only Singleton services can be resolved by constructor injection in Middleware. You should move the dependency to the Invoke method like this:

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);
    }
}

Wow! Never knew you could inject into methods, is this just for middleware or can I use this trick in my own methods?
What about IMiddleware that's registered as scoped? I know for sure I get a new instance of middleware, but I still cant inject a scoped service to it.
@FergalMoran Unfortunately, this "trick" is a special behavior of just the middleware's Invoke method. However, you can achieve something similar via the autofac IoC lib and property injection. See ASP.NET Core MVC Dependency Injection via property or setter method?.
Injection is not magic. There is an engine behind the scenes that actually calls the dependency container to generate instances to pass as parameters to either constructors or methods. This particual engine looks for methods named "Invoke" with a first argument of HttpContext and then creates instances for the rest of the parameters.
"only Singleton services can be resolved by constructor injection in Middleware". Just learned a new thing! (and solved my problem :-)
H
Hakan Fıstık

Another way to get the instance of scoped dependency is to inject service provider (IServiceProvider) into the middleware constructor, create scope in Invoke method and then get the required service from the scope:

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

    //do your stuff....
}

Check out Resolving Services in a Method Body in asp.net core dependency injection best practices tips tricks for more details.


Super helpful, thanks! For anybody trying to access EF Contexts in middleware this is the way to go as they are scoped by default.
stackoverflow.com/a/49886317/502537 does this more directly
At first I didn't think this worked, but then I realized you're doing scope.ServiceProvider instead of _serviceProvider on the second line. Thanks for this.
Don't forget using Microsoft.Extensions.DependencyInjection; if you want to use ServiceProvider.CreateScope(). See here: docs.microsoft.com/en-us/dotnet/api/…
When using a scoped service in an asp.net filter, and then trying to get the same scoped service in a singleton service method, the scope provided by the method shown here is not the same one that was used in the filter.
J
Joe Audette

Middleware is always a singleton so you can't have scoped dependencies as constructor dependencies in the constructor of your middleware.

Middleware supports method injection on the Invoke method,so you can just add the IEmailRepository emailRepository as a parameter to that method and it will be injected there and will be fine as scoped.

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

    ....
}

I was in similar situation, then I added service using AddTransient, and it was able to resolve the dependency. I thought it wouldn't work since the middleware is singleton? bit strange..
I think a Transient dependency would have to be disposed manually, unlike scoped which will be automatically disposed at the end of the web request where it is first created. Maybe a transient disposable inside a scoped dependency would get disposed the the outer object is disposed. Still I'm not sure a transient dependency inside a singleton or an object with a longer than transient lifetime is a good idea, think I would avoid that.
Even though you can inject a Transient-scoped dependency via the constructor in this case, it will not get instantiated as you would think. It will only happen once, when the Singleton is built.
You have mentioned that middleware is always a singleton, but it is not true. It is possible to create a middleware as a factory-based middleware and use it as a scoped-middleware.
Looks like factory based middleware was introduced in asp.netcore 2.2 and documentation was created in 2019. So my answer was true when I posted it as far as I know. Factory based middleware does look like a good solution today.
H
Harun Diluka Heshan

Your middleware and the service has to be compatible with each other in order to inject the service via the constructor of your middleware. Here, your middleware has been created as a convention-based middleware which means it acts as a singleton service and you have created your service as scoped-service. So, you cannot inject a scoped-service into the constructor of a singleton-service because it forces the scoped-service to act as a singleton one. However, here are your options.

Inject your service as a parameter to the InvokeAsync method. Make your service a singleton one, if possible. Transform your middleware to a factory-based one.

A Factory-based middleware is able to act as a scoped-service. So, you can inject another scoped-service via the constructor of that middleware. Below, I have shown you how to create a factory-based middleware.

This is only for demonstration. So, I have removed all the other code.

public class Startup
{
    public Startup()
    {
    }

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

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

The TestMiddleware:

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

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

The TestService:

public class TestService
{
}