HostedService:无法跟踪实体类型的实例

时间:2019-02-11 16:21:59

标签: c# entity-framework asp.net-core .net-core entity-framework-core

我正在使用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核心的配置有误导性?预先感谢您的支持。

1 个答案:

答案 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。