Is there any way to make ITicketStore scoped?

Viewed 258

I am using ITicketStore as per this article. In this DbContext is created whenever required to perform any database operations.

In my code instead of creating the DbContext by using() syntax, injecting through the constructor. Everything was working fine until the code goes production. Started getting bellow exceptions when traffic increased.

  • System.InvalidOperationException A second operation started on this context before a previous operation completed.
  • Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException

This probably because ITicketStore is a singleton and injecting DbContext using DI that shared the same DbContext instance between multiple threads.

I changed the code to create a DbContext instance by using() and it's working fine now.

But I am trying to find out any way to make DbContext work by injecting through DI.

1 Answers

Here is the way I did it

//in Startup.cs ConfigureServices method  

//I tried using the PostConfigure methods, but it didn't to work for me
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptionsTicketStore>());
    //This sets the store to the options.
    public class PostConfigureCookieAuthenticationOptionsTicketStore : IPostConfigureOptions<CookieAuthenticationOptions>
    {
        private readonly SingletonTicketStore _store;

        public PostConfigureCookieAuthenticationOptionsTicketStore(SingletonTicketStore store) => _store = store;

        public void PostConfigure(string name, CookieAuthenticationOptions options) => options.SessionStore = _store;
    }
    
    //This implemenation is singleton compatible
    public class SingletonTicketStore : ITicketStore
    {
        private readonly IServiceProvider _services;

        public SingletonTicketStore(IServiceProvider services)
        {
            _services = services;
        }

        public async Task RemoveAsync(string key) => await WithScopeAsync(s => s.RemoveAsync(key));
        public async Task RenewAsync(string key, AuthenticationTicket ticket) => await WithScopeAsync(s => s.RenewAsync(key, ticket));
        public async Task<AuthenticationTicket> RetrieveAsync(string key) => await WithScopeAsync(s => s.RetrieveAsync(key));
        public async Task<string> StoreAsync(AuthenticationTicket ticket) => await WithScopeAsync(s => s.StoreAsync(ticket));

        private async Task WithScopeAsync(Func<ITicketStore, Task> action) => await WithScopeAsync(async t => { await action(t); return true; });
        private async Task<T> WithScopeAsync<T>(Func<ITicketStore, Task<T>> action)
        {
            //This opens a new scope, allowing you to use scoped dependencies
            using (var scope = _services.CreateScope())
            {
                //don't forget to await the result here, of stuff (dbcontext) could be disposed otherwise
                return await action(scope.ServiceProvider.GetRequiredService<ITicketStore>());
            }
        }
    }

    public class ScopedTicketStore : ITicketStore
    {
        //your implementation
    }

Hope this helps ;)

Related