Ho un WebApi che utilizza EFCore2.0 e 2 client che tentano di accedere a un metodo di azione contemporaneamente ... Tutto funziona perfettamente con un client. Ma quando 2 o più tentativo di accedere a un metodo di azione particolare allo stesso tempo, ho ricevuto questo errore in Microsoft.EntityFrameworkCore:
Una seconda operazione è iniziata in questo contesto prima che una precedente operazione fosse completata. Non è garantito che tutti i membri di istanza siano thread-safe
Ho usato DI e repository per WebApi. Ho definito Scope per IUnitOfWork, ho definito Transient, ma non funziona nulla.
Questa è la mia 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();
});
});
....
Questo è il mio 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();
}
}
}
Questo mio 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());
}
}
Questo è uno dei miei repository:
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);
}
.....
E infine, questo è uno dei miei 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;
}
}
}
}
e questa è la mia traccia di stack:
in Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection () in Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.d__61.MoveNext () --- Fine dello stack trace dalla posizione precedente in cui è stata generata un'eccezione --- su System.Runtime. ExceptionServices.ExceptionDispatchInfo.Throw () su System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (Task task) su System.Runtime.CompilerServices.TaskAwaiter1.GetResult () su Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.d__59.MoveNext () - - Fine dello stack trace dalla posizione precedente in cui è stata generata un'eccezione --- su System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () su System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (Task dell'attività) su System.Runtime.CompilerServices.TaskAwaiter1 .GetResult () in Microsoft.EntityFrameworkCore.DbContext.d__48.MoveNext () --- Fine della traccia dello stack dalla posizione precedente in cui è stata generata un'eccezione --- a System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () su System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (Task task) su System.Runtime.CompilerServices.TaskAwaiter1.GetResult () su Eela.Service.LogRepository.d__7.MoveNext () in D: \ Eela \ Eela.Service \ LogRepository.cs: riga 41 --- Traccia fine dello stack dalla posizione precedente in cui è stata generata un'eccezione --- su System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () su System.Runtime.CompilerServices .TaskAwaiter.HandleNonSuccessAndDebuggerNotification (Task task) su System.Runtime.CompilerServices.TaskAwaiter1.GetResult () su Eela.Web.Models.RequestLoggingMiddleware.d__5.MoveNext () in D: \ Eela \ Eela.Web \ Models \ RequestLoggingMiddleware.cs: linea 82
Aggiornare :
Ho un middleware nel mio startup.cs:
app.UseMiddleware<RequestLoggingMiddleware>();
Quando commento, il mio codice funziona senza alcun problema. Includo la fonte RequestLoggingMiddleware.cs nella mia domanda.
Dov'è il problema principale?
La mia ipotesi è che il middleware sia istanziato solo una volta. Quindi, ciò significa che viene utilizzata una singola istanza di contesto per eseguire l'accesso simultaneo al database.
Ci sono due soluzioni. Il primo consiste nel disporre di una struttura di contesto e creare un'istanza di contesto ogni volta che viene chiamato il metodo Invoke
. Il secondo è quello di memorizzare i record di log nel middleware, in una raccolta. E salvarli in un database a determinate condizioni (il numero di record ha raggiunto un determinato numero o il timeout ha raggiunto lo zero).
A seconda della quantità di record di log, potrebbero verificarsi problemi di prestazioni. Il secondo approccio richiede l'implementazione di un accesso concorrente corretto alla raccolta. E in alcune circostanze puoi perdere un certo numero di record di log.