I've build a background task in my ASP.NET Core 2.1 following this tutorial: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#consuming-a-scoped-service-in-a-background-task
Compiling gives me an error:
System.InvalidOperationException: 'Cannot consume scoped service 'MyDbContext' from singleton 'Microsoft.AspNetCore.Hosting.Internal.HostedServiceExecutor'.'
What causes that error and how to fix it?
Background task:
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>();
Program.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(); }
Full startup.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"); } } }
The Solution. To be able to use scoped services within a singleton, you must create a scope manually. A new scope can be created by injecting an IServiceScopeFactory into your singleton service (the IServiceScopeFactory is itself a singleton, which is why this works).
You should almost never consume scoped service or transient service from a singleton. You should also avoid consuming transient service from a scoped service.
When you register implementations of IHostedService using any of the AddHostedService extension methods - the service is registered as a singleton. There may be scenarios where you'd like to rely on a scoped service. For more information, see Dependency injection in .
First, DbContext is a lightweight object; it is designed to be used once per business transaction. Making your DbContext a Singleton and reusing it throughout the application can cause other problems, like concurrency and memory leak issues. And the DbContext class is not thread safe.
You need to inject IServiceScopeFactory to generate a scope. Otherwise you are not able to resolve scoped services in a singleton.
using (var scope = serviceScopeFactory.CreateScope()) { var context = scope.ServiceProvider.GetService<MyDbContext>(); }
Edit: It's perfectly fine to just inject IServiceProvider
and do the following:
using (var scope = serviceProvider.CreateScope()) // this will use `IServiceScopeFactory` internally { var context = scope.ServiceProvider.GetService<MyDbContext>(); }
The second way internally just resolves IServiceProviderScopeFactory
and basically does the very same thing.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With