Rozwiązanie błędu "EF tracking entity with same ID"

0

Hej, mam problem uzywajac EF6.

Zapisywanie encji szlo standardowo od controllera, az do repozytorium i zapis do DB przy uzyciu EF.
Niestety doszly do tego rozne background taski, ktore synchronizuja wiele rzeczy, w tym tez zapisuja encje do DB - tak jak tez wolaja serwisy, repo, itd.
Kod jest ogromny (w wiekszosci nie moj, teraz dorabiam te background taski) - w wielu miejscach poza repo (flow jest controller->service->repo) jest mapowanie na encje uzywane przez DB, przez co (podejrzewam) dbContext trakcuje te encje (lokalnie) - i tu pojawia sie problem - bo jak ktorys background task probuje zapisac encje, mam blad, ze encja o tym ID juz jest trackowana (sorki nie mam tego bledu pod reka)
co dziwniejszego, jak sobie przeiteruje po tym localu po ID, to jest kilka encji z tym samym ID trackowane

fixuje to w taki sposob na poziomie repo przy zapisie

 foreach (var type in types)
{
    var trackedEntities = applicationDbContext.Set<TypeEntity>().Local.Where(x => x.Id == type.Id).ToArray();
    if (trackedEntities != null && trackedEntities.Any())
    {
        foreach(var e in trackedEntities)
        applicationDbContext.Entry(e).State = EntityState.Detached;
    }
}

ale czy tak sie powinno...?

1

Jak są tworzone background taski? Coś tu podejrzanie wygląda. Dla spokoju zacząłbym od tego, że Task w tle powinien opierować na innej instancji DbContextu, mającej inny scope.

0

ma inny dbcontext i ma tworzony scope
zaczynam od rejestracji

public static void ConfigureBackgroundTasksDI(this IServiceCollection services)
{
    services.AddHostedService<ConsumeScopedServiceHostedService>();
    services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
    services.AddScoped<ISynchronizationBackgroundService, SynchronizationBackgroundService>();
    services.AddScoped<ISyncAllBackgroundService, SyncAllBackgroundService>();
    services.AddScoped<IMissionSynchronizationService, MissionSynchronizationBackgroundService>();
    services.AddScoped<IMessageTypeSynchronizationService, MessageTypesSynchronizationService>();
    services.AddScoped<IMissionsChecklistSynchronizationService, MissionsChecklistSynchronization>();
    services.AddSingleton<IEntitiesUpdatesNotifier, EntitiesUpdatesNotifier>();
}

klasa jak z dokumentacji

  public class ConsumeScopedServiceHostedService : BackgroundService
{
    public IServiceProvider Services { get; }
    public ConsumeScopedServiceHostedService(IServiceProvider services)
    {
        Services = services;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService =
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }
}

ScopedProcessingService:

public interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}
public class ScopedProcessingService : IScopedProcessingService
{
    private readonly ISyncAllBackgroundService _syncAllService;
    private readonly IMissionSynchronizationService _missionSyncService;
    private readonly IMessageTypeSynchronizationService _messageTypeSynchronizationService;
    private readonly IMissionsChecklistSynchronizationService _missionsChecklistSynchronizationService;
    private readonly IConfiguration _configuration;

    public ScopedProcessingService(ISyncAllBackgroundService syncService,
        IConfiguration configuration,
        IMissionSynchronizationService missionSyncService,
        IMessageTypeSynchronizationService messageTypeSynchronizationService,
        IMissionsChecklistSynchronizationService missionsChecklistSynchronizationService)
    {
        _syncAllService = syncService;
        _configuration = configuration;
        _missionSyncService = missionSyncService;
        _messageTypeSynchronizationService = messageTypeSynchronizationService;
        _missionsChecklistSynchronizationService = missionsChecklistSynchronizationService;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        var tasks = new[]
        {
            Sync(stoppingToken, _syncAllService.Synchronize, GetLoopSyncAllIntervalInMiliseconds),
            Sync(stoppingToken,  _missionSyncService.Synchronize , GetLoopSyncCreatedEntitiesInMiliseconds),
            Sync(stoppingToken, _messageTypeSynchronizationService.Synchronize, GetLoopSyncCreatedEntitiesInMiliseconds),
            Sync(stoppingToken, _missionsChecklistSynchronizationService.Synchronize, GetLoopSyncCreatedEntitiesInMiliseconds)
        };
      
        await Task.WhenAll(tasks);
    }


 
    private async Task Sync(CancellationToken stoppingToken,Func<Task> task, Func<int> getIntervalMiliseconds)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var ms = getIntervalMiliseconds();
            if (ms > 0)
            {
                try
                {
                    await task();
                }
                catch (Exception ex)
                {
                    Log.Error($"SYNCHRONIZATION ERROR {ex.Message} {ex.InnerException?.Message}");
                }
            }
            Thread.Sleep(ms);
        }
    }

I potem juz kazdy wstrzyknietyt serwis (do tych wstrzyknietych tutaj backgroundServicow) ma wstrzykiwany m.in. dbContext (rejestrowany jako transient)

protected virtual void AddDbContext(IServiceCollection services)
{
    string connectionString = GetConnectionString();
    services.AddDbContext<ApplicationDbContext>(item => item.UseSqlServer(connectionString,
      sqlServerOptions => sqlServerOptions.CommandTimeout(300)), contextLifetime: ServiceLifetime.Transient);
}
0

Nie widać co jest w poszczególnych serwisach, ale sądzę że wychodzą Ci z tego singletony, ponieważ ConsumeScopedServiceHostedService tworzy jeden Scope odpalany podczas uruchamiania aplikacji.

0

sorki, ale odpisze tutaj, bo w komentarzu sie nie zmiesci
czy lepiej bedzie zamiast wolac :

using (var scope = Services.CreateScope())
{
    var scopedProcessingService =
        scope.ServiceProvider
            .GetRequiredService<IScopedProcessingService>();

    await scopedProcessingService.DoWork(stoppingToken);
}

na tym poziomie zrobic kilka scopow dla kazdego z mojego "backgroundSerwisu", tj. jeden backgroundService = jeden scope ?

2

Tak byłoby lepiej. A jeszcze lepiej w ogóle wyciągnąć z serwisów wewnętrzne pętle z interwałami i tworzyć Scope wtedy kiedy usługa ma być wywołana.

0

ok, dzieki pokminie cos :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1