【问题标题】:HostedService: The instance of entity type cannot be trackedHostedService:无法跟踪实体类型的实例
【发布时间】:2019-02-11 16:21:59
【问题描述】:

我正在使用 asp.net core 2.2 和 ef core 2.2.1 开发一个 web api。该 api 除了处理 Angular 应用程序发出的 restful 请求外,还负责处理一些用作与其他软件接口的 xml 文件。文件在应用服务器本地,通过FileWatcher 检测到。

我在测试期间注意到,当我多次重新处理一个 xml 测试文件时,从第二次重新处理该文件开始,我得到了异常:

System.InvalidOperationException:实体类型的实例 无法跟踪“QualityLot”,因为另一个实例具有密钥 值“{QualityLotID: ...}”已被跟踪。什么时候 附加现有实体,确保只有一个实体实例具有 附加给定的键值。

当我调用DbContext.QualityLot.Update(qualityLot);方法时

“处理文件”服务及其使用的服务配置到Startup.cs文件中,如下所示:

services.AddHostedService<InterfaceDownloadService>();
services.AddTransient<IQLDwnldService, QLDwnldService>();

db 上下文配置如下:

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);是因为实体已经存在于db中

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 核心的误导性配置?提前感谢您的支持。

【问题讨论】:

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


    【解决方案1】:

    老实说,我无法弄清楚您的 DBContext 是从您的代码中实际注入的位置。

    但是从错误消息中,我想说您的上下文在不应该出现的地方被重用。所以它被注射一次,然后一遍又一遍地使用。

    您已将服务注册为“Scoped”(因为这是默认设置)。

    您应该将其注册为“Transient”,以确保每次调用您的服务提供商时都会获得一个新实例:

    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。

    【讨论】:

    • 感谢您的回答。 DBContext其实是通过QLDwnldService的构造函数注入的。我会尝试一下你的建议并告诉你
    • 谢谢你,确实有效。您认为以这种方式工作可能存在任何性能问题吗?
    • 这不是一个好的解决方案。它可能会起作用,但现在整个应用程序中的所有 DbContext 实例都是作用域 Transient ,这可能是不需要的(由于某种原因,Scoped 是默认值)。或者,使用_ServiceProvider.CreateScope()processFiles() 方法中创建一个新的IServiceScope,然后从这个新服务范围解析 DbContext 和所有其他服务。
    • 我认为作用域 DbContext 更令人困惑。除了 OP 的问题,context 不能用于同时异步查询。而是创建一个 DbContextFactory 类,注入它,每次需要上下文时创建新实例。 DbContext 只是一个类,不会打开额外的连接;)。
    • @Brad 你说得对,稍后我会在使用真正的键盘时添加:)
    猜你喜欢
    • 2019-10-28
    • 2017-06-30
    • 1970-01-01
    • 2021-05-04
    • 2022-01-26
    • 2023-01-03
    • 1970-01-01
    • 2017-09-09
    • 1970-01-01
    相关资源
    最近更新 更多