【问题标题】:Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'无法使用单例“Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor”中的范围服务“MyDbContext”
【发布时间】:2019-01-08 03:31:18
【问题描述】:

我按照本教程在我的 ASP.NET Core 2.1 中构建了一个后台任务:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#consuming-a-scoped-service-in-a-background-task

编译给我一个错误:

System.InvalidOperationException: '不能使用单例'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor' 中的范围服务'MyDbContext'。'

导致该错误的原因以及如何解决?

后台任务:

internal class OnlineTaggerMS : IHostedService, IDisposable
{
    private readonly CoordinatesHelper _coordinatesHelper;
    private Timer _timer;
    public IServiceProvider Services { get; }

    public OnlineTaggerMS(IServiceProvider services, CoordinatesHelper coordinatesHelper)
    {
        Services = services;
        _coordinatesHelper = coordinatesHelper;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Run every 30 sec
        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(30));

        return Task.CompletedTask;
    }

    private async void DoWork(object state)
    {
        using (var scope = Services.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

            Console.WriteLine("Online tagger Service is Running");

            // Run something
            await ProcessCoords(dbContext);
        }          
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    private async Task ProcessCoords(MyDbContext dbContext)
    {
        var topCoords = await _coordinatesHelper.GetTopCoordinates();

        foreach (var coord in topCoords)
        {
            var user = await dbContext.Users.SingleOrDefaultAsync(c => c.Id == coord.UserId);

            if (user != null)
            {
                var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                //expire time = 120 sec
                var coordTimeStamp = DateTimeOffset.FromUnixTimeMilliseconds(coord.TimeStamp).AddSeconds(120).ToUnixTimeMilliseconds();

                if (coordTimeStamp < now && user.IsOnline == true)
                {
                    user.IsOnline = false;
                    await dbContext.SaveChangesAsync();
                }
                else if (coordTimeStamp > now && user.IsOnline == false)
                {
                    user.IsOnline = true;
                    await dbContext.SaveChangesAsync();
                }
            }
        }
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Startup.cs:

services.AddHostedService<OnlineTaggerMS>();

程序.cs:

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

        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            try
            {
                var context = services.GetRequiredService<TutorDbContext>();
                DbInitializer.Initialize(context);
            }
            catch(Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred while seeding the database.");
            }
        }

        host.Run();
    }

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

完整的启动.cs:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy",
                builder => builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });

        services.AddDbContext<MyDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        // ===== Add Identity ========
        services.AddIdentity<User, IdentityRole>()
            .AddEntityFrameworkStores<TutorDbContext>()
            .AddDefaultTokenProviders();

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
        services
            .AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = Configuration["JwtIssuer"],
                    ValidAudience = Configuration["JwtIssuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
                    ClockSkew = TimeSpan.Zero // remove delay of token when expire
                };
            });

        //return 401 instead of redirect
        services.ConfigureApplicationCookie(options =>
        {
            options.Events.OnRedirectToLogin = context =>
            {
                context.Response.StatusCode = 401;
                return Task.CompletedTask;
            };

            options.Events.OnRedirectToAccessDenied = context =>
            {
                context.Response.StatusCode = 401;
                return Task.CompletedTask;
            };
        });

        services.AddMvc();

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new Info { Version = "v1", Title = "xyz", });

            // Swagger 2.+ support
            var security = new Dictionary<string, IEnumerable<string>>
            {
                {"Bearer", new string[] { }},
            };

            c.AddSecurityDefinition("Bearer", new ApiKeyScheme
            {
                Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
                Name = "Authorization",
                In = "header",
                Type = "apiKey"
            });
            c.AddSecurityRequirement(security);
        });

        services.AddHostedService<OnlineTaggerMS>();
        services.AddTransient<UsersHelper, UsersHelper>();
        services.AddTransient<CoordinatesHelper, CoordinatesHelper>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IServiceProvider serviceProvider, IApplicationBuilder app, IHostingEnvironment env, TutorDbContext dbContext)
    {
        dbContext.Database.Migrate();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseCors("CorsPolicy");
        app.UseAuthentication();
        app.UseMvc();

        app.UseSwagger();
        app.UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint("v1/swagger.json", "xyz V1");
        });

        CreateRoles(serviceProvider).GetAwaiter().GetResult();
    }

    private async Task CreateRoles(IServiceProvider serviceProvider)
    {
        var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
        var UserManager = serviceProvider.GetRequiredService<UserManager<User>>();
        string[] roleNames = { "x", "y", "z", "a" };
        IdentityResult roleResult;

        foreach (var roleName in roleNames)
        {
            var roleExist = await RoleManager.RoleExistsAsync(roleName);
            if (!roleExist)
            {
                roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
            }
        }
        var _user = await UserManager.FindByEmailAsync("xxx");

        if (_user == null)
        {
            var poweruser = new User
            {
                UserName = "xxx",
                Email = "xxx",
                FirstName = "xxx",
                LastName = "xxx"
            };
            string adminPassword = "xxx";

            var createPowerUser = await UserManager.CreateAsync(poweruser, adminPassword);
            if (createPowerUser.Succeeded)
            {
                await UserManager.AddToRoleAsync(poweruser, "xxx");
            }
        }
    }

【问题讨论】:

  • 请参阅:idownvotedbecau.se/nomcve。我实际上并没有投反对票,因为很明显您做出了善意的努力,但您仍然需要遵循该链接上的指导。特别是,发布您的堆栈跟踪和/或突出显示代码中异常的确切来源。

标签: c# asp.net asp.net-mvc asp.net-core


【解决方案1】:

您需要注入IServiceScopeFactory 来生成作用域。否则,您将无法在单例中解析范围服务。

using (var scope = serviceScopeFactory.CreateScope())
{
  var context = scope.ServiceProvider.GetService<MyDbContext>();
}

编辑: 只需注入 IServiceProvider 并执行以下操作就可以了:

using (var scope = serviceProvider.CreateScope()) // this will use `IServiceScopeFactory` internally
{
  var context = scope.ServiceProvider.GetService<MyDbContext>();
}

第二种方式在内部只是解析IServiceProviderScopeFactory,基本上做同样的事情。

【讨论】:

  • IServiceProvider 在这里应该可以正常工作,如教程所示。我之前尝试过使用 IServiceScopeFactory (stackoverflow.com/questions/51616771/…) 并得到了相同的结果。
  • 您可以尝试将 dbcontext 添加为瞬态吗?就像一个测试。 services.AddTransient();
  • @alsami,这是将 DbContext 添加为 Transient 的错误方法,AddDbContext 方法的第二个参数是服务生命周期。所以 AddDbContext(options => {}, ServiceLifetime.Transient)
  • @Inari,不,不是。此 AddDbContext 只是为 dbcontext 的基本构造函数传递委托的扩展。只是传递连接字符串和其他选项的一种方法,但我感谢您的努力。
  • 将 dbcontext 添加为瞬态没有帮助。实际上,当我将 dbcontext 添加为单例时,应用程序已启动,但我想这不是一种方法吗?
【解决方案2】:

对于实体框架 DbContext 类,现在有一个更好的方法:

services.AddDbContextFactory<MyDbContext>(options => options.UseSqlServer(...))

然后你可以像这样在你的单例类中注入工厂:

 IDbContextFactory<MyDbContext> myDbContextFactory

最后像这样使用它:

using var myDbContex = _myDbContextFactory.CreateDbContext();

【讨论】:

  • 迄今为止发布的最简单的解决方案
【解决方案3】:

虽然@peace 的回答对他有用,但如果您的IHostedService 中有DBContext,您需要使用IServiceScopeFactory

要查看如何执行此操作的惊人示例,请查看此答案How should I inject a DbContext instance into an IHostedService?

如果您想从博客中了解更多信息,请check this out

【讨论】:

    【解决方案4】:

    我找到了错误的原因。它是CoordinatesHelper 类,用于后台任务OnlineTaggerMS 并且是Transient - 所以它导致了错误。我不知道为什么编译器一直抛出指向MyDbContext 的错误,让我在几个小时内偏离轨道。

    【讨论】:

    • @Virodh 我将CoorinatesHelper 设为单身。你不能依赖一个生命周期比你的对象更短的对象。你可以在这里阅读更多信息:dotnetcoretutorials.com/2018/03/20/…
    【解决方案5】:

    您仍然需要向服务提供商注册MyDbContext。通常这样做是这样的:

    services.AddDbContext<MyDbContext>(options => {
        // Your options here, usually:
        options.UseSqlServer("YourConnectionStringHere");
    });
    

    如果您还发布了您的 Program.cs 和 Startup.cs 文件,它可能会更清楚一些事情,因为我能够快速设置一个测试项目来实现代码并且无法重现您的问题。

    【讨论】:

    • 正如错误所说,正在解决依赖关系,但范围不匹配。必须注册上下文。
    • 确实,上下文已注册。我按照您的要求添加了完整的 startup.cs。
    • @PeaceSMC 你的 Startup.cs 的使用块是什么......我怀疑它可能是 IHostingEnvironment 之间的冲突,因为在 Microsoft.Extensions.Hosting (AddHostingService 所在的位置)中都有指定和 ASP.NET Core 的默认设置。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-05
    • 2018-01-30
    • 2021-01-11
    相关资源
    最近更新 更多