无法从根提供程序解析作用域服务。 .Net Core 2

230

当我尝试运行我的应用程序时,出现错误

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

奇怪的是,就我所知,这个EmailRepository和接口与我所有其他的存储库完全相同,但对它们不会抛出任何错误。只有在我尝试使用app.UseEmailingExceptionHandling();行时才会发生错误。以下是我的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();
    }
}

这里是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;
        }
    }
}

最后是异常处理中间件

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

更新: 我发现这个链接https://github.com/aspnet/DependencyInjection/issues/578,它让我修改了我的Program.cs文件中的BuildWebHost方法,从以下内容:


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

到这里

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

我不知道具体发生了什么,但现在似乎能正常工作。


4
发生的情况是,作用域嵌套并未得到验证;也就是说,在运行时它不会检查您的作用域级别是否存在不当嵌套。显然,在1.1中默认关闭了此功能。一旦2.0出现,他们就默认打开了它。 - Robert Burke
任何试图关闭ValidateScopes的人,请阅读此链接https://dev59.com/gVUL5IYBdhLWcg3wi4eT#50198738。 - Yorro
5个回答

414

您在Startup类中将IEmailRepository注册为作用域服务。这意味着您无法将其作为构造函数参数注入到Middleware中,因为只有Singleton服务可以在Middleware中通过构造函数注入解析。您应该将依赖项移动到Invoke方法中,像这样:

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

43
哇!我从未想到过可以在方法中进行注入,这只能用于中间件吗?还是我也可以在自己的方法中使用这个技巧? - Fergal Moran
注册为作用域的IMiddleware怎么样?我确定会得到一个新的中间件实例,但我仍然无法将作用域服务注入其中。 - macwier
16
很抱歉,这个"技巧"只适用于中间件的Invoke方法。不过,您可以通过使用autofac IoC库和属性注入来实现类似的效果。请参阅ASP.NET Core MVC通过属性或setter方法进行依赖注入? - Felix K.
9
注入并不是魔法。实际上,有一个引擎在幕后调用依赖容器生成实例,并将其作为构造函数或方法的参数传递。该引擎查找名称为“Invoke”的方法,并将第一个参数设为HttpContext,然后为其他参数创建实例。 - Thanasis Ioannidis
5
“只有Singleton服务才能在中间件中通过构造函数注入解决。”学到了新东西!(并且解决了我的问题 :-)) - Michel
有趣的是,我正在使用一个中间件通过构造函数注入UserManager,只有在开发环境中它才会在启动时崩溃,其他环境则不会。谢谢! - CularBytes

221

另一种获取作用域依赖项实例的方法是将服务提供程序 (IServiceProvider) 注入到中间件构造函数中,在 Invoke 方法中创建 scope,然后从作用域中获取所需的服务:

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

    //do your stuff....
}

请查看asp.net core依赖注入最佳实践技巧中的在方法体中解析服务,以获取更多细节。


9
非常有帮助,谢谢!对于任何试图在中间件中访问EF上下文的人来说,这是默认范围的正确方式。 - ntziolis
4
一开始我觉得这不起作用,但后来意识到你在第二行使用的是scope.ServiceProvider而不是_serviceProvider。谢谢你。 - adam0101
我认为最好使用 IServiceScopeFactory 来实现这个目的。 - Francesco D.M.
2
如果您想使用 ServiceProvider.CreateScope(),请不要忘记添加 using Microsoft.Extensions.DependencyInjection;。请参阅此处:https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.createscope?view=dotnet-plat-ext-5.0 - Mark Cooper
1
当在asp.net过滤器中使用作用域服务,然后尝试在单例服务方法中获取相同的作用域服务时,此处显示的方法提供的作用域与过滤器中使用的作用域不同。 - Richard Collette
显示剩余4条评论

45

中间件始终是单例的,因此您不能将范围依赖项作为构造函数依赖项添加到中间件的构造函数中。

中间件支持在Invoke方法上进行方法注入,因此您可以将IEmailRepository emailRepository作为该方法的参数添加到其中,它将被注入并且在作用域上也没问题。

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

    ....
}

1
我曾经遇到过类似的情况,然后我使用AddTransient添加了服务,它能够解决依赖关系。我本以为这不会起作用,因为中间件是单例的?有点奇怪... - Sateesh Pagolu
1
我认为瞬态依赖项需要手动处理,而作用域依赖项将在其首次创建的 Web 请求结束时自动处理。也许作用域依赖项内部的瞬态可处理对象会在外部对象被处理时一并处理。但我不确定在单例或生命周期长于瞬态的对象中使用瞬态依赖项是否是一个好主意,我想我会避免这样做。 - Joe Audette
2
即使在这种情况下可以通过构造函数注入瞬态作用域的依赖项,但它不会像您想象的那样被实例化。 它只会在构建单例时发生一次。 - Jonathan
3
您提到过中间件始终是单例的,但这并不正确。可以创建基于工厂的中间件,并将其用作作用域中间件。 - Harun Diluka Heshan
1
似乎工厂基础的中间件是在asp.netcore 2.2中引入的,并且文档是在2019年创建的。因此,据我所知,当我发布答案时,我的回答是正确的。工厂基础的中间件看起来是一个不错的解决方案。 - Joe Audette

11
您的中间件服务必须相互兼容,才能通过中间件构造函数注入服务。在这里,您的中间件被创建为一种基于约定的中间件,这意味着它作为一个单例服务,而您已将其服务创建为范围服务。因此,您无法将范围服务注入到单例服务的构造函数中,因为这会强制范围服务表现为一个单例服务。但是,以下是您的一些选择:
  1. 将您的服务作为参数注入到InvokeAsync方法中。
  2. 如果可能,请将您的服务设为单例服务。
  3. 将您的中间件转换为基于工厂的中间件。
基于工厂的中间件可以作为范围服务运行。因此,您可以通过该中间件的构造函数注入另一个范围服务。下面,我展示了如何创建一个基于工厂的中间件,仅供演示,因此我已删除了所有其他代码。
public class Startup
{
    public Startup()
    {
    }

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

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

TestMiddleware

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

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

TestService

public class TestService
{
}

8
在 .NET Core 6 中,以下设置对我有效:
using (var scope = app.Services.CreateScope())
 {
     var services = scope.ServiceProvider.GetRequiredService<IDbInitilizer>;
     services.Invoke().Initialize();
 }

DBInitilizer


就记录而言,我也在使用.NET 6,并且接受的答案对我来说非常有效,无需手动创建服务范围。但是也许在这个特定的上下文中(在program.cs本身内部)它不起作用。 - k3davis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接