Multiple Concurrency access to DbContext in Webapi

asp.net-core asp.net-core-webapi c# entity-framework entity-framework-core

Question

I have a WebApi that that uses EFCore2.0 and 2 client trying to access to an action method at a same time ... Everything is working fine with one client. But when 2 or more trying to access to a one particular action method at a same time, I got this error at Microsoft.EntityFrameworkCore :

A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe

I used DI and repository for WebApi. I defined Scope for IUnitOfWork,I defined Transient, but nothing is worked.

This is my startup :

....
services.AddSingleton(provider => Configuration);
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddTransient<IRoleService, RoleService>();
            services.AddTransient<ISecurityService, SecurityService>();
            services.AddTransient<IDbInitializerService, DbInitializerService>();
            services.AddTransient<ITokenStoreService, TokenStoreService>();
            services.AddTransient<ITokenValidatorService, TokenValidatorService>();
            services.AddTransient<ICookieValidatorService, CookieValidatorService>();
            services.AddTransient<IRequestRepository, RequestRepository>();
            services.AddDbContextPool<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration["ConnectionStrings:ApplicationDbContextConnection"].ToString(),
                    serverDbContextOptionsBuilder =>
                    {
                        var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                        serverDbContextOptionsBuilder.CommandTimeout(minutes);
                        serverDbContextOptionsBuilder.EnableRetryOnFailure();
                    });
            });
....

This is my DbContext:

namespace Eela.Data
{

    public class
        ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {


        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var services = new ServiceCollection();
            services.AddOptions();
            services.AddScoped<IHostingEnvironment, CustomHostingEnvironment>();
            services.AddSingleton<ILoggerFactory, LoggerFactory>();
            var serviceProvider = services.BuildServiceProvider();
            var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
            var logger = loggerFactory.CreateLogger<ConfigProvider>();
            var hostingEnvironment = serviceProvider.GetRequiredService<IHostingEnvironment>();
            Console.WriteLine($"Using `{hostingEnvironment.ContentRootPath}` as the ContentRootPath");
            var configuration = new ConfigurationBuilder()
                .SetBasePath(basePath: hostingEnvironment.ContentRootPath)
                .AddJsonFile(path: "appsettings.json", reloadOnChange: true, optional: false)
                .AddEncryptedProvider(hostingEnvironment: hostingEnvironment, logger: logger)
                .AddJsonFile(path: $"appsettings.{hostingEnvironment.EnvironmentName}.json", optional: true)
                .Build();            
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration["ConnectionStrings:ApplicationDbContextConnection"];
            var useInMemoryDatabase = configuration[key: "UseInMemoryDatabase"].Equals(value: "true",
                comparisonType: StringComparison.OrdinalIgnoreCase);
            if (useInMemoryDatabase)
                builder.UseInMemoryDatabase("MyDatabase");
            else
            builder.UseSqlServer(connectionString);
            builder.ConfigureWarnings(warnings => warnings.Log(CoreEventId.IncludeIgnoredWarning));
            return new ApplicationDbContext(builder.Options);
        }
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {

        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }
        protected override void OnModelCreating(ModelBuilder model)
        {
            base.OnModelCreating(model);

            model.Entity<Person>().Property(p => p.PersonId).ValueGeneratedOnAdd();
            model.Entity<Person>()
                .HasDiscriminator<int>(name: "Type")
                .HasValue<WorkerTaxi>(value: Convert.ToInt32(value: AccountType.TaxiWorker))
                .HasValue<User>(value: Convert.ToInt32(value: AccountType.User))
                .HasValue<Reseller>(value: Convert.ToInt32(value: AccountType.Reseller));

            model.Entity<Log>().Property(p => p.Id).ValueGeneratedOnAdd();
            model.Entity<Log>()
                .HasDiscriminator<int>(name: "Type")
                .HasValue<LogRequest>(value: Convert.ToInt32(value: LogLevel.Information))
                .HasValue<LogError>(value: Convert.ToInt32(value: LogLevel.Error));

            model.Entity<Request>().Property(p => p.RequestId).ValueGeneratedOnAdd();
            model.Entity<Request>()
                .HasDiscriminator<int>(name: "Type")
                .HasValue<RequestTaxi>(value: Convert.ToInt32(value: RequestType.TaxiRequester));

            model.Entity<ApplicationUsers>().Property(p => p.Id).ValueGeneratedOnAdd();
            model.Entity<Role>().Property(p => p.RoleId).ValueGeneratedOnAdd();
            model.Entity<Car>().Property(p => p.CarId).ValueGeneratedOnAdd();
            model.Entity<Address>().Property(p => p.AddressId).ValueGeneratedOnAdd();
            model.Entity<Organization>().Property(p => p.OrganizationId).ValueGeneratedOnAdd();
            model.Entity<Credit>().Property(p => p.CreditId).ValueGeneratedOnAdd();
            model.Entity<StablePrice>().Property(p => p.StablePriceId).ValueGeneratedOnAdd();
            model.Entity<Package>().Property(p => p.PackageId).ValueGeneratedOnAdd();
            model.Entity<Rating>().Property(p => p.RatingId).ValueGeneratedOnAdd();
            model.Entity<City>().Property(p => p.CityId).ValueGeneratedOnAdd();
            model.Entity<SpecialAddress>().Property(p => p.SpecialAddressId).ValueGeneratedOnAdd();
            model.Entity<UserToken>().Property(p => p.Id).ValueGeneratedOnAdd();
            model.Entity<PersonRequest>(entity =>
            {
                entity.HasKey(e => new {e.RequestId, e.PersonId})
                    .HasName(name: "PK_dbo.PersonRequest");

                entity.HasIndex(e => e.RequestId)
                    .HasName(name: "IX_RequestId");

                entity.HasIndex(e => e.PersonId)
                    .HasName(name: "IX_PersonId");
            });
            model.Entity<PackageReseller>(entity =>
            {
                entity.HasKey(e => new { e.PackageId, e.ResellerId })
                    .HasName(name: "PK_dbo.PackageReseller");

                entity.HasIndex(e => e.PackageId)
                    .HasName(name: "IX_PackageId");

                entity.HasIndex(e => e.ResellerId)
                    .HasName(name: "IX_ResellerId");
            });
            model.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.ApplicationUserId, e.RoleId })
                    .HasName(name: "PK_dbo.UserRole");

                entity.HasIndex(e => e.ApplicationUserId)
                    .HasName(name: "IX_ApplicationUserId");

                entity.HasIndex(e => e.RoleId)
                    .HasName(name: "IX_RoleId");
            });
        }
        public virtual DbSet<ApplicationUsers> ApplicationUsers { get; set; }
        public virtual DbSet<Role> Role { get; set; }
        public virtual DbSet<UserRole> UserRole { get; set; }
        public virtual DbSet<UserToken> UserToken { get; set; }

        public virtual DbSet<Address> Address { get; set; }
        public virtual DbSet<Credit> Credit { get; set; }
        public virtual DbSet<Organization> Organization { get; set; }
        public virtual DbSet<City> City { get; set; }
        public virtual DbSet<StablePrice> StablePrice { get; set; }
        public virtual DbSet<PersonRequest> PersonRequest { get; set; }
        public virtual DbSet<Discount> Discount { get; set; }
        public virtual DbSet<Rating> Rating { get; set; }
        public virtual DbSet<SpecialAddress> SpecialAddress { get; set; }


        public void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class
        {
            Set<TEntity>().AddRange(entities: entities);
        }

        public void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class
        {
            Set<TEntity>().RemoveRange(entities: entities);
        }

        public void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class
        {
            Entry(entity: entity).State = EntityState.Modified; // Or use ---> this.Update(entity);
        }

        public void ExecuteSqlCommand(string query)
        {
            Database.ExecuteSqlCommand(sql: query);
        }

        public void ExecuteSqlCommand(string query, params object[] parameters)
        {
            Database.ExecuteSqlCommand(sql: query, parameters: parameters);
        }

        public int SaveAllChanges()
        {
            return SaveChanges();
        }

        public Task<int> SaveAllChangesAsync()
        {
            return SaveChangesAsync();
        }
    }
}

This my IUnitOfWork:

namespace Eela.Data
{
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
        void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;

        EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
        void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;

        void ExecuteSqlCommand(string query);
        void ExecuteSqlCommand(string query, params object[] parameters);

        int SaveAllChanges();
        Task<int> SaveAllChangesAsync();

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }
}

This is one of my repositories :

public class RequestRepository : IRequestRepository
   {
       private readonly IMapper _mapper;
       private readonly IUnitOfWork _unitOfWork;
       private readonly DbSet<Request> _request;
       private readonly DbSet<Person> _person;
       private readonly DbSet<PersonRequest> _personRequest;
       public RequestRepository(IMapper mapper, IUnitOfWork unitOfWork)
       {
           _mapper = mapper;
           _unitOfWork = unitOfWork;
           _request = _unitOfWork.Set<Request>();
           _person = _unitOfWork.Set<Person>();
           _personRequest = _unitOfWork.Set<PersonRequest>();
       }
     public async Task<DetailPageViewModel> GetRequestAsync(string requestId)
       {
           var request = await (from x in _request
               where x.RequestId == Guid.Parse(requestId)
               from y in x.PersonsRequests
               where y.Person is User
               select new DetailPageViewModel
               {
                   RequestId = x.RequestId.ToString(),
                   CustomerName = y.Person.LastName,
                   SourceAddress = ((RequestTaxi) x).SourceAddress,
                   DestinationAddress = ((RequestTaxi) x).DestinationAddress,
                   DestinationLat = x.DestinationLat,
                   DestinationLon = x.DestinationLon,
                   EstimateDistance = ((RequestTaxi) x).Distance.ToString(CultureInfo.InvariantCulture),
                   EstimateDriverPrice = x.Price.ToString(),
                   EstimatePassengerPrice = x.PaymentType == PaymentType.Cash ? x.Price.ToString() : "0",
                   SourceLat = ((RequestTaxi) x).SourceLat,
                   SourceLon = ((RequestTaxi) x).SourceLon
               }).FirstOrDefaultAsync();

           return
               _mapper.Map<DetailPageViewModel>(
                   source: request);
       }
       .....

And finally, this is one of my controller :

public class DetailPageController:Controller
   {
       private readonly IPersonRequestRepository _personRequest;
       private readonly IRequestRepository _request;
       private readonly IApplicationUsersRepository _appUser;
       private readonly IStablePriceRepository _stablePrice;
       private readonly ILogRepository _log;
       private readonly ICreditRepository _credit;
       private readonly INotificationService _notification;
       private readonly IPasswordGenerator _charecterGenerator;

       public DetailPageController(IPersonRequestRepository personRequest,ICreditRepository credit,
           ILogRepository log,IStablePriceRepository stablePrice,IApplicationUsersRepository appUser,
           IRequestRepository request,INotificationService notification,IPasswordGenerator charecterGenerator)
       {
           _personRequest = personRequest;
           _credit = credit;
           _log = log;
           _stablePrice = stablePrice;
           _appUser = appUser;
           _request = request;
           _notification = notification;
           _charecterGenerator = charecterGenerator;
       }

       [HttpPost]
       [ActionName("GetRequest")]
       public async Task<ActionResult> GetRequest([FromBody]string model)
       {
           var requestId = model;
           return Json(data: await _request.GetRequestAsync(requestId));
       }

RequestLoggingMiddleware.cs :

public class RequestLoggingMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<RequestLoggingMiddleware> _logger;
        private readonly ILogRepository _logRepository;
        private readonly IConfigurationRoot _configuration;

        public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger,
            ILogRepository logRepository,IConfigurationRoot configuration)
        {
            _next = next;
            _logger = logger;
            _logRepository = logRepository;
            _configuration = configuration;
        }

        public async Task<OperationResult> Invoke(HttpContext context)
        {
            using (MemoryStream requestBodyStream = new MemoryStream())
            {
                using (MemoryStream responseBodyStream = new MemoryStream())
                {
                    Stream originalRequestBody = context.Request.Body;
                    context.Request.EnableRewind();
                    Stream originalResponseBody = context.Response.Body;

                    OperationResult op= new OperationResult();
                    try
                    {
                        await context.Request.Body.CopyToAsync(requestBodyStream);
                        requestBodyStream.Seek(0, SeekOrigin.Begin);

                        string requestBodyText = new StreamReader(requestBodyStream).ReadToEnd();

                        requestBodyStream.Seek(0, SeekOrigin.Begin);
                        context.Request.Body = requestBodyStream;

                        string responseBody = "";


                        context.Response.Body = responseBodyStream;
                        Stopwatch watch = Stopwatch.StartNew();
                        await _next(context);
                        watch.Stop();

                        responseBodyStream.Seek(0, SeekOrigin.Begin);
                        responseBody = new StreamReader(responseBodyStream).ReadToEnd();
                        var log = new LogRequestViewModel
                        {
                            Host= context.Request.Host.Host,
                            Path= context.Request.Path,
                            QueryString= context.Request.QueryString.ToString(),
                            ClientIp= context.Connection.RemoteIpAddress.MapToIPv4(),
                            Date= DateTime.Now.ToString(CultureInfo.InvariantCulture),
                            Duration= watch.ElapsedMilliseconds,
                            Method= context.Request.Method,
                            RequestContentLength= context.Request.ContentLength,
                            RequestContentType= context.Request.ContentType,
                            Application= GetType().Namespace,
                            User= context.User.Claims
                                .FirstOrDefault(x => x.Type == _configuration["UserIdType"])?.Value,
                            Headers= string.Join(",", context.Request.Headers.Select(he => he.Key + ":[" + he.Value + "]").ToList()),
                            RequestBodyText= requestBodyText,
                            ResponseBodyText = responseBody

                        };
                        var result = await _logRepository.SaveRequestLogAsync(log);
                        if (!result.Success)
                        {
                            op.Success = false;
                            op.AddMessage("Couldn't add request log to database");
                            _logger.LogError(message: result.MessageList.FirstOrDefault());
                            var ex = new Exception(message: result.MessageList.FirstOrDefault());
                            await _logRepository.SaveErrorLogAsync(exception: ex);
                        }
                        responseBodyStream.Seek(0, SeekOrigin.Begin);

                        await responseBodyStream.CopyToAsync(originalResponseBody);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(message: ex.Message);
                        await _logRepository.SaveErrorLogAsync(exception: ex);
                        byte[] data = System.Text.Encoding.UTF8.GetBytes("Unhandled Error occured, the error has been logged and the persons concerned are notified!! Please, try again in a while.");
                        originalResponseBody.Write(data, 0, data.Length);
                        op.Success = false;
                        op.AddMessage(ex.Message);
                    }
                    finally
                    {
                        context.Request.Body = originalRequestBody;
                        context.Response.Body = originalResponseBody;

                    }
                    const string logTemplate = @"
                        Client IP: {clientIP}
                        Request path: {requestPath}
                        Request content type: {requestContentType}
                        Request content length: {requestContentLength}
                        Start time: {startTime}
                        Duration: {duration}";
                    _logger.LogInformation(logTemplate,
                        context.Connection.RemoteIpAddress.ToString(),
                        context.Request.Path,
                        context.Request.ContentType,
                        context.Request.ContentLength,
                        DateTime.UtcNow,
                        Stopwatch.StartNew());
                    return op;

                }

            }
        }
    }

and this is my stack trace :

at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection() at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.d__61.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.d__59.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Microsoft.EntityFrameworkCore.DbContext.d__48.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Eela.Service.LogRepository.d__7.MoveNext() in D:\Eela\Eela.Service\LogRepository.cs:line 41 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Eela.Web.Models.RequestLoggingMiddleware.d__5.MoveNext() in D:\Eela\Eela.Web\Models\RequestLoggingMiddleware.cs:line 82

Update :

I have a Middleware at my startup.cs :

app.UseMiddleware<RequestLoggingMiddleware>();

When I Comment it out, My code works without any problem. I include RequestLoggingMiddleware.cs source in my question.

Where is the main problem ?

1
0
11/14/2017 8:47:20 AM

Accepted Answer

My guess is that the middleware is instantiated just once. So, that means that a single instance of context is used to perform concurrent access to database.

There are two solutions. The first one is to have context fabric and create an instance of context each time Invoke method is called. The second one is to store log records in the middleware, in a collection. And save them to database on some condition(count of records reached some particular number or timeout has reached zero).

Depending on amount of log records, you might have performance issues. The second approach requires implementation of correct concurrent access to the collection. And in some circumstances you can lost some amount of log records.

1
11/14/2017 11:25:40 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow