我正在使用asp.net core 2.2和ef core 2.2.1开发一个Web api。除了处理有角度的应用程序发出的烦人请求外,该api还负责处理一些xml文件,这些文件用作与其他软件的接口。文件是应用程序服务器的本地文件,并通过FileWatcher
进行检测。
我在测试过程中注意到,当我多次重新处理xml测试文件时,从第二次重新处理该文件开始,我得到了异常:
System.InvalidOperationException:实体类型的实例 无法跟踪“ QualityLot”,因为另一个具有密钥的实例 值'{QualityLotID:...}'已被跟踪。什么时候 附加现有实体,请确保只有一个具有 附加了给定的键值。
当我调用方法DbContext.QualityLot.Update(qualityLot);
将“处理文件”服务及其使用的服务配置到Startup.cs
文件中,如下所示:
services.AddHostedService<InterfaceDownloadService>();
services.AddTransient<IQLDwnldService, QLDwnldService>();
数据库上下文的配置如下:
services.AddDbContext<MyDbContext>(cfg =>
{
cfg.UseSqlServer(_config.GetConnectionString("LIMSConnectionString"));
});
该类如下:
public class InterfaceDownloadService : BackgroundServiceBase
{
[...]
public InterfaceDownloadService(IHostingEnvironment env,
ILogger<InterfaceDownloadService> logger,
IServiceProvider serviceProvider)
{
_ServiceProvider = serviceProvider;
}
[...]
private void processFiles()
{
[...]
_ServiceProvider.GetService<IQLDwnldService>().QLDownloadAsync(ev);
}
}
public abstract class BackgroundServiceBase : IHostedService, IDisposable
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts =
new CancellationTokenSource();
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it,
// this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts.Cancel();
}
}
在关键点上,我有一个例外:
public async Task QLDownloadAsync(FileReceivedEvent fileReceivedEvent)
{
Logger.LogInformation($"QLDwnld file {fileReceivedEvent.Event.FullPath} received for Processing");
try
{
QualityLotDownload qualityRoutingDwnld = deserializeObject<QualityLotDownload>(fileReceivedEvent.XsltPath, fileReceivedEvent.Event.FullPath);
Logger.LogDebug($"QLDwnld file {fileReceivedEvent.Event.FullPath} deserialized correctly. Need to determinate whether Insert or Update QualityLot {qualityRoutingDwnld.QualityLots.QualityLot.QualityLotID}");
for (int remainingRetries = fileReceivedEvent.MaxRetries; remainingRetries > 0; remainingRetries--)
{
using (var transaction = await DbContext.Database.BeginTransactionAsync())
{
try
{
var qualityLotDeserialized = qualityRoutingDwnld.QualityLots.QualityLot;
// insert the object into the database
var qualityLot = await DbContext.QualityLot.Where(x => x.QualityLotID == qualityLotDeserialized.QualityLotID).FirstOrDefaultAsync();
if (qualityLot == null) // INSERT QL
{
await InsertQualityLot(qualityLotDeserialized);
}
else // UPDATE QL
{
await UpdateQualityLot(qualityLot, qualityLotDeserialized);
}
[...]
transaction.Commit();
}
catch (Exception ex)
{
Logger.LogError(ex, $"Retry {fileReceivedEvent.MaxRetries - remainingRetries +1}: Exception processing QLDwnld file {fileReceivedEvent.Event.FullPath}.");
transaction.Rollback();
if (remainingRetries == 1)
{
return;
}
}
因为实体已存在于数据库中,所以调用了方法UpdateQualityLot(qualityLot, qualityLotDeserialized);
private async Task UpdateQualityLot(QualityLot qualityLot, QualityLotDownloadQualityLotsQualityLot qualityLotDeserialized)
{
[fields update]
DbContext.QualityLot.Update(qualityLot);
await DbContext.SaveChangesAsync();
}
对DbContext.QualityLot.Update(qualityLot);
的调用失败。
对于所有正在处理的文件,从中我可以看到QLDwnldService
的实例是新的,换句话说,每次有新对象(在Startup.cs中配置)时,以下方法都会返回
_ServiceProvider.GetService<IQLDwnldService>().QLDownloadAsync(ev);
,当DbContext被重用时,这可能就是已跟踪实体结果的原因。
我也试图在DbContext OnConfiguring()
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
所以我的问题是。怎么了架构可能有问题,还是e核心的配置有误导性?预先感谢您的支持。
答案 0 :(得分:1)
说实话,我无法弄清楚从代码中实际注入了DBContext的位置。
但是从错误消息中,我会说您的上下文在不应该使用的地方被重用。因此,将其注入一次,然后反复使用。
您已将服务注册为“范围”(因为这是默认设置)。
您应该将其注册为“临时”,以确保每次致电服务提供商时都会获得一个新实例:
services.AddDbContext<MyDbContext>(cfg =>
{
cfg.UseSqlServer(_config.GetConnectionString("LIMSConnectionString"));
},
ServiceLifetime.Transient);
Brad提到,这将对您的其余申请产生影响,他是对的。
更好的选择可能是保留DbContext
的范围,并将IServiceScopeFactory
注入托管服务。然后在需要的地方创建一个新范围:
using(var scope = injectedServiceScopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<DbContext>();
// do your processing with context
} // this will end the scope, the scoped dbcontext will be disposed here
请注意,这仍然并不意味着您应该并行访问DbContext。我不知道您的通话为何全部不同步。如果您实际上在进行并行工作,请确保为每个线程创建一个DbContext。