简体   繁体   中英

HostedService: The instance of entity type cannot be tracked

I'm developing a web api with asp.net core 2.2 and ef core 2.2.1. The api, besides handling the restful requests made by an angular app, is in charge of processing some xml files that are used as an interface with other software. Files are local to the application server and detected through a FileWatcher .

I've noticed during my tests that when I reprocess multiple times an xml test file, starting from the second time the file is being reprocessed, I obtain the Exception:

System.InvalidOperationException: The instance of entity type 'QualityLot' cannot be tracked because another instance with the key value '{QualityLotID: ...}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

when I call the method DbContext.QualityLot.Update(qualityLot);

The "processing file" service and the service it's using are configured into the Startup.cs file as following:

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

the db context is configued like this:

services.AddDbContext<MyDbContext>(cfg =>
{                
    cfg.UseSqlServer(_config.GetConnectionString("LIMSConnectionString"));
});

and the class looks like:

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();
    }
}

Here the critical point, where I have the exception:

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;
                    }
                }

The method UpdateQualityLot(qualityLot, qualityLotDeserialized); is invoked because the entity already exists in the db

private async Task UpdateQualityLot(QualityLot qualityLot, QualityLotDownloadQualityLotsQualityLot qualityLotDeserialized)
{
    [fields update]
    DbContext.QualityLot.Update(qualityLot);
    await DbContext.SaveChangesAsync();
}

The call to DbContext.QualityLot.Update(qualityLot); fails.

From what I can see the instance of QLDwnldService is new for every file being processed, in other words the following method returns every time a new object (as configured into Startup.cs)

_ServiceProvider.GetService<IQLDwnldService>().QLDownloadAsync(ev);

, while the DbContext is reused and that's probably the reason why the entity results already tracked.

I also tride to setup the non-tracking option in the DbContext OnConfiguring()

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    base.OnConfiguring(optionsBuilder);
    optionsBuilder
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);  
}

So my question is. What's wrong here? Maybe an architecture problematic or maybe a misleading configuration of e core? Thanks in advance for any support.

To be honest I could not figure out where your DBContext is actually injected from your code.

But from the error message I'd say your context is reused in a place where it should not be. So it's injected once and then used over and over and over.

You have registered your service as "Scoped" (because that's the default).

You should register it as "Transient" to ensure you will get a new instance on every call to your service provider:

services.AddDbContext<MyDbContext>(cfg =>
{                
    cfg.UseSqlServer(_config.GetConnectionString("LIMSConnectionString"));
}, 
ServiceLifetime.Transient);

Brad mentioned that this will have consequences for the rest of your application and he's right.

The better option might be to leave your DbContext scoped and inject the IServiceScopeFactory into your hosted service. Then create a new scope where you need it:

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

Please note that this still does not mean that you should access the DbContext in parallel. I don't know why your calls are all async. If you are actually doing parallel work, make sure you create one DbContext per thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM