55

I've build a background task in my ASP.NET Core 2.1 following this tutorial: https://learn.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");
            }
        }
    }
Maciej Wanat
  • 1,527
  • 1
  • 11
  • 23

5 Answers5

65

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.

alsami
  • 8,996
  • 3
  • 25
  • 36
  • 2
    [IServiceProvider](https://msdn.microsoft.com/pl-pl/library/system.iserviceprovider(v=vs.110).aspx) should work just fine here, as tutorial shows. I tried with IServiceScopeFactory before (https://stackoverflow.com/questions/51616771/system-invalidoperationexception-cannot-consume-scoped-service-mydbcontext-fr) and got the same results. – Maciej Wanat Jul 31 '18 at 17:45
  • Can you try to add the dbcontext as transient? Just as a test. services.AddTransient(); – alsami Jul 31 '18 at 17:54
  • @alsami, that is the incorrect way to add a DbContext as Transient, the second parameter to the AddDbContext method is the Service lifetime. So AddDbContext(options => {}, ServiceLifetime.Transient) – Inari Jul 31 '18 at 17:57
  • @Inari, no it is not. This AddDbContext is just an extension to pass a delegate for the base constructor of dbcontext. Is just one way to pass a connectionstring and other options but I appreciate your effort. – alsami Jul 31 '18 at 17:59
  • Adding dbcontext as transient doesn't help. Actually application started when I added dbcontext as a singleton, but I guess it's not a way to go? – Maciej Wanat Jul 31 '18 at 18:14
  • No, because changes will not be saved with a singleton and you will run into lot of problems. Wait I will try it myself. – alsami Jul 31 '18 at 18:15
  • So for me it works as transient. I figured it must be your hostedservice registration. It is an extension method, what does it do? here the sample https://github.com/alsami/hostedservice-dbcontext-sample – alsami Jul 31 '18 at 18:39
  • In case you have an interface, you could use GetRequiredService instead of GetService using (var scope = serviceScopeFactory.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); } – PAS Sep 10 '19 at 01:14
  • You can always use `GetRequiredService()`. Only difference is, when registration can't be found, `GetRequiredService()` won't. – alsami Sep 19 '19 at 06:46
23

For Entity Framework DbContext classes there is a better way now with:

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

Then you can just inject the factory in your singleton class like this:

 IDbContextFactory<MyDbContext> myDbContextFactory

And finally use it like this:

using var myDbContex = _myDbContextFactory.CreateDbContext();
Greg Z.
  • 1,376
  • 14
  • 17
  • But in case of multi-tenant (multi database), how would will we able to get connection string from SaaS database for initialization of other context of Tenant Database? – Nabeel Sep 06 '22 at 23:43
10

Though @peace answer worked for him, if you do have a DBContext in your IHostedService you need to use a IServiceScopeFactory.

To see an amazing example of how to do this check out this answer How should I inject a DbContext instance into an IHostedService?.

If you would like to read more about it from a blog, check this out .

thalacker
  • 2,389
  • 3
  • 23
  • 44
4

I found the reason of an error. It was the CoordinatesHelper class, which is used in the the background task OnlineTaggerMS and is a Transient - so it resulted with an error. I have no idea why compiler kept throwing errors pointing at MyDbContext, keeping me off track for few hours.

Maciej Wanat
  • 1,527
  • 1
  • 11
  • 23
  • @Virodh I made `CoorinatesHelper` a singleton. You can't depend on an object with shorter lifetime then your object. You can read more about it here: https://dotnetcoretutorials.com/2018/03/20/cannot-consume-scoped-service-from-singleton-a-lesson-in-asp-net-core-di-scopes/ – Maciej Wanat Feb 27 '19 at 11:46
-4

You still need to register MyDbContext with the service provider. Usually this is done like so:

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

If you also posted your Program.cs and Startup.cs files, it may shed some more light on things, as I was able to quickly setup a test project implementing the code and was unable to reproduce your issue.

Inari
  • 556
  • 3
  • 14
  • As the error says, the dependency is being resolved but the scope doesn't match. Context therefor must be registered. – alsami Jul 31 '18 at 17:29
  • Indeed, context is registered. I added full startup.cs as you requested. – Maciej Wanat Jul 31 '18 at 17:43
  • @PeaceSMC what is the using block for your Startup.cs ... I suspect it could be a conflict between IHostingEnvironment, as there is on specified in both Microsoft.Extensions.Hosting (where AddHostingService lives) and the default one for ASP.NET Core. – Inari Jul 31 '18 at 18:01