Wie werden DataContext-Instanzen in einer Consumer-Anwendung zwischengespeichert?


8

Wir haben eine Anwendung, die das von unserem Anbieter bereitgestellte SDK verwendet, um es einfach in sie zu integrieren. Dieses SDK stellt eine Verbindung zum AMQP-Endpunkt her und verteilt, zwischenspeichert und transformiert Nachrichten einfach an unsere Kunden. Zuvor erfolgte diese Integration über HTTP mit XML als Datenquelle, und bei der alten Integration gab es zwei Möglichkeiten, DataContext zwischenzuspeichern - pro Webanforderung und pro verwalteter Thread-ID. (1)

Jetzt integrieren wir jedoch nicht über HTTP, sondern AMQP, was für uns transparent ist, da das SDK die gesamte Verbindungslogik übernimmt und wir nur noch unsere Konsumenten definieren müssen, sodass es keine Option gibt, DataContext "pro Webanforderung" zwischenzuspeichern Nur pro verwalteter Thread-ID bleibt übrig. Ich habe ein Chain-of-Responsibility-Muster implementiert. Wenn ein Update bei uns eingeht, wird es in eine Pipeline von Handlern gestellt, die DataContext verwenden, um die Datenbank gemäß den neuen Updates zu aktualisieren. So sieht die Aufrufmethode der Pipeline aus:

public Task Invoke(TInput entity)
{
    object currentInputArgument = entity;

    for (var i = 0; i < _pipeline.Count; ++i)
    {
        var action = _pipeline[i];
        if (action.Method.ReturnType.IsSubclassOf(typeof(Task)))
        {
            if (action.Method.ReturnType.IsConstructedGenericType)
            {
                dynamic tmp = action.DynamicInvoke(currentInputArgument);
                currentInputArgument = tmp.GetAwaiter().GetResult();
            }
            else
            {
                (action.DynamicInvoke(currentInputArgument) as Task).GetAwaiter().GetResult();
            }
        }
        else
        {
            currentInputArgument = action.DynamicInvoke(currentInputArgument);
        }
    }

    return Task.CompletedTask;
}

Das Problem ist (zumindest was ich denke), dass diese Verantwortungskette eine Kette von Methoden ist, die neue Aufgaben zurückgeben / starten. Wenn also ein Update für Entität A kommt, wird es von der verwalteten Thread-ID = 1 behandelt, sagen wir, und erst einige Zeit später Wiederum kommt dieselbe Entität A nur an, um beispielsweise von der verwalteten Thread-ID = 2 behandelt zu werden . Dies führt zu:

System.InvalidOperationException: 'Ein Entitätsobjekt kann nicht von mehreren Instanzen von IEntityChangeTracker referenziert werden.'

weil DataContext von der verwalteten Thread-ID = 1 bereits Entität A verfolgt (zumindest denke ich, dass dies so ist)

Meine Frage ist, wie ich DataContext in meinem Fall zwischenspeichern kann. Hattest du das gleiche Problem? Ich habe dies und diese Antworten gelesen und nach meinem Verständnis ist die Verwendung eines statischen DataContext ebenfalls keine Option. (2)

  1. Haftungsausschluss: Ich hätte sagen sollen, dass wir die Anwendung geerbt haben und ich kann nicht antworten, warum sie so implementiert wurde.
  2. Haftungsausschluss 2: Ich habe wenig bis gar keine Erfahrung mit EF.

Die Gemeinschaft stellte Fragen:

  1. Welche Version von EF verwenden wir? 5.0
  2. Warum leben Entitäten länger als der Kontext? - Sie tun es nicht, aber vielleicht fragen Sie sich, warum Entitäten länger leben müssen als der Kontext. Ich verwende Repositorys, die zwischengespeicherten DataContext verwenden, um Entitäten aus der Datenbank abzurufen und sie in einer In-Memory-Sammlung zu speichern, die ich als Cache verwende.

Auf diese Weise werden Entitäten "extrahiert", wobei DatabaseDataContextsich der zwischengespeicherte DataContext befindet, über den ich spreche (BLOB mit ganzen Datenbanksätzen im Inneren).

protected IQueryable<T> Get<TProperty>(params Expression<Func<T, TProperty>>[] includes)
{
    var query = DatabaseDataContext.Set<T>().AsQueryable();

    if (includes != null && includes.Length > 0)
    {
        foreach (var item in includes)
        {
            query = query.Include(item);
        }
    }

    return query;
}

Immer wenn meine Verbraucheranwendung eine AMQP-Nachricht empfängt, beginnt mein Verantwortungskettenmuster zu prüfen, ob diese Nachricht und ihre Daten, die ich bereits verarbeitet habe. Ich habe also eine Methode, die so aussieht:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
            where TEntity : ISportEvent
{
    ... some unimportant business logic

    //save the sport
    if (sport.SportID > 0) // <-- this here basically checks if so called 
                           // sport is found in cache or not
                           // if its found then we update the entity in the db
                           // and update the cache after that
    {
        _sportRepository.Update(sport); /* 
                                         * because message update for the same sport can come
                                         * and since DataContext is cached by threadId like I said
                                         * and Update can be executed from different threads
                                         * this is where aforementioned exception is thrown
                                        */

    }
    else                   // if not simply insert the entity in the db and the caches
    {
        _sportRepository.Insert(sport);
    }

    _sportRepository.SaveDbChanges();

    ... updating caches logic
}

Ich dachte, dass das Abrufen von Entitäten aus der Datenbank mit einer AsNoTracking()Methode oder das Trennen von Entitäten jedes Mal, wenn ich eine Entität "aktualisiere" oder "einfüge", dies lösen würde, aber dies war nicht der Fall.


Nicht, dass ich noch keine Antwort habe, können Sie mir bitte sagen, welche Version von EF Sie verwenden
Simon Price

Schauen Sie sich das auch an und sehen Sie, ob dies Ihnen überhaupt hilft. stackoverflow.com/questions/41346635/…
Simon Price

@ SimonPrice, 5.0
Kuskmen

Sie können Entität A nach dem Aktualisieren entfernen. Aber dies wird Ihr Parallelitätsproblem nicht lösen,
sondern

@ilkerkaran, aber wenn ich nach dem Update / Einfügen die Spur verlasse, heißt das nicht, dass ich es später nicht in der Datenbank speichern kann? Ich rufe grundsätzlich Update oder Insert basierend auf Kriterien auf und dann sofort gefolgt von SaveChanges.
Kuskmen

Antworten:


2

Während das Aktualisieren eines DbContext einen gewissen Overhead mit sich bringt und die Verwendung von DI zum Freigeben einer einzelnen Instanz eines DbContext innerhalb einer Webanforderung einen Teil dieses Overheads einsparen kann, können einfache CRUD-Operationen für jede Aktion nur einen neuen DbContext neu erstellen.

Wenn ich mir den Code ansehe, den Sie bisher gepostet haben, würde ich wahrscheinlich eine private Instanz des DbContext im Repository-Konstruktor neu erstellen und dann für jede Methode ein Repository neu erstellen.

Dann würde Ihre Methode ungefähr so ​​aussehen:

public async Task<TEntity> Handle<TEntity>(TEntity sportEvent)
        where TEntity : ISportEvent
{
        var sportsRepository = new SportsRepository()

        ... some unimportant business logic

        //save the sport
        if (sport.SportID > 0) 
        {
            _sportRepository.Update(sport);
        }
        else
        {
            _sportRepository.Insert(sport);
        }

        _sportRepository.SaveDbChanges();

}

public class SportsRepository
{
    private DbContext _dbContext;

    public SportsRepository()
    {
        _dbContext = new DbContext();
    }

}

Möglicherweise möchten Sie auch die Verwendung von Stub-Entitäten in Betracht ziehen , um einen DbContext für andere Repository-Klassen freizugeben.


Ja, leider wird das Datenschichtprojekt sowohl von unserem neuen Dienst als auch von der alten Website-Anwendung verwendet und kann nicht geändert werden. : / Am Ende habe ich einen Singleton-Datenbankkontext für meinen gesamten Ablauf verwendet, und ich denke, ich werde daran denken, dies später zu ändern, wenn wir unsere Pipeline multithreaded machen
kuskmen

0

Da es sich um eine vorhandene Geschäftsanwendung handelt, werde ich mich auf Ideen konzentrieren, die zur Lösung des Problems beitragen können, anstatt Vorlesungen über Best Practices zu halten oder architektonische Änderungen vorzuschlagen.

Ich weiß, dass dies offensichtlich ist, aber manchmal hilft uns das Umformulieren von Fehlermeldungen besser zu verstehen, was los ist.

Die Fehlermeldung zeigt an, dass Entitäten von mehreren Datenkontexten verwendet werden, was darauf hinweist, dass mehrere dbcontext-Instanzen vorhanden sind und dass Entitäten von mehr als einer dieser Instanzen referenziert werden.

Dann heißt es in der Frage, dass es pro Thread einen Datenkontext gibt, der sich früher pro http-Anforderung befand, und dass Entitäten zwischengespeichert werden.

Es scheint also sicher anzunehmen, dass Entitäten bei einem Cache-Fehler aus einem Datenbankkontext gelesen und bei einem Treffer aus dem Cache zurückgegeben werden. Der Versuch, Entitäten zu aktualisieren, die von einer Datenbankkontextinstanz mit einer zweiten Datenbankkontextinstanz geladen wurden, führt zu dem Fehler. Wir können daraus schließen, dass in diesem Fall in beiden Operationen genau dieselbe Entitätsinstanz verwendet wurde und keine Serialisierung / Deserialisierung für den Zugriff auf den Cache vorhanden ist.

DbContext-Instanzen sind an sich Entitäts-Caches über ihren internen Änderungsverfolgungsmechanismus, und dieser Fehler ist ein Schutz zum Schutz seiner Integrität. Da die Idee darin besteht, einen lang laufenden Prozess zu haben, der gleichzeitige Anforderungen über mehrere Datenbankkontexte (einen pro Thread) und einen Cache für gemeinsam genutzte Entitäten verarbeitet, wäre dies in Bezug auf Leistung und Speicher sehr vorteilhaft (die Änderungsverfolgung würde wahrscheinlich den Speicherverbrauch mit der Zeit erhöhen ) um zu versuchen, entweder den Lebenszyklus von Datenbankkontexten so zu ändern, dass er pro Nachricht erfolgt, oder den Änderungs-Tracker zu leeren, nachdem jede Nachricht verarbeitet wurde.

Um Entitätsaktualisierungen verarbeiten zu können, müssen sie natürlich direkt nach dem Abrufen aus dem Cache und bevor Änderungen auf sie angewendet werden, an den aktuellen Datenbankkontext angehängt werden.


Vielen Dank für die Erkenntnisse, ich stimme Ihnen zu, aber das Problem bleibt hier. Meine aktuelle Problemumgehung besteht darin, "Repository" zu überspringen und direkt mit dem Datenkontext zu arbeiten ...
kuskmen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.