C# Async und Sync Methoden. Doppelten Code vermeiden

Magic1416

Lieutenant
Registriert
Dez. 2003
Beiträge
513
Hi,

ich habe eine Designfrage zu Async und Sync Methoden. Ich arbeite gerade an einer Repository Klasse für EntityFramework Core. Ich möchte eine Methode Synchron und Asynchron bereitstellen und dabei doppelten Code vermeiden. Gemäß Posting auf c# - Async and sync versions of method - Stack Overflow ist es zum Beispiel schlecht, Code so zu schreiben:

C#:
Public static Task DownloadToCacheAsync()
{
    return Task.Run(()=>DownloadToCache());
}

Public static void DownloadToCache()
{
    DownloadToCacheAsync().Wait();
}

Ich habe folgende zwei Methoden und möchte hier sinnvoll auf doppelten Code verzichten:

C#:
public Environments CreateEnvironment(string name, int objectId)
        {
            //get env by unique id
            var environment = _context.Enivronments.Where(p => p.ObjektID == objectId).FirstOrDefault();


            //check if env is already in database
            if (environment == null)
            {
                //create env
                environment = new Environments()
                {
                    Name = name,
                    ObjektID = objectId,
                    IsActive = false,
                    Delay = 0,
                    Sequence = 999
                };
                _context.Enivronments.Add(environment);
            }
            else
            {
                //Update env Name, if it has changed
                if (environment.Name != name)
                    environment.Name = name;
            }
            return environment;
        }

        public async Task<Environments> CreateEnvironmentAsync(string name, int objectId)
        {
            //get env by unique id
            var environment = await _context.Enivronments.Where(p => p.ObjektID == objectId).FirstOrDefaultAsync();
           

            //check if env is already in database
            if (environment == null)
            {
                //create enve
                environment = new Environments()
                {
                    Name = name,
                    ObjektID = objectId,
                    IsActive = false,
                    Delay = 0,
                    Sequence = 999
                };
                await _context.Enivronments.AddAsync(environment);
            }
            else
            {
                //Update env Name, if it has changed
                if (environment.Name != name)
                    environment.Name = name;
            }
            return environment;
        }

Was ist nun der programmatisch richtige Ansatz um doppelten Code zu vermeiden.

Gruß Magic
 
ist zwar nicht genau das was du meinst,
aber nutze doch die SOLID Methode für deinen Code. Damit wirst du niemals doppelten code schreiben.
 
Ich würde mich als erstes Fragen, ob du überhaupt noch synchrone Methoden anbieten solltest.
Wenn du das synchrone Interface anbieten musst, dann hast du mehrere Möglichkeiten:
  1. Dein erstes Beispiel nutzen und deinen (a)synchronen Code wrappen. Das sollte man aber tunlichst vermeiden. Dann lieber alles asynchron anbieten und der aufrufende hat das Problem.
  2. Deine Code beibehalten und Code doppelt vorhalten und pflegen.
  3. T4-Templates zum Generieren des Codes nutzen und somit den Code nur einmal pflegen und dann generieren lassen. Schwierigeres Programmieren durch mehr Komplexität.
  4. Während der Laufzeit zwischen synchron und asynchron wechseln. Langsamer als eine synchrone und asynchrone Implementierung.
Beispiel für Variante 4:

C#:
public Environments CreateEnvironment(string name, int objectId)
{
    // we know for sure that this code runs synchronously, therefore we can wait for the result synchronously
    return CreateEnvironmentInternalAsync(name, objectId, isAsynchronous: false).GetAwaiter().GetResult();
}

public Task<Environments> CreateEnvironmentAsync(string name, int objectId)
{
    return CreateEnvironmentInternalAsync(name, objectId, isAsynchronous: true);
}

private async Task<Environments> CreateEnvironmentInternalAsync(string name, int objectID, bool runAsynchronously)
{
    //get env by unique id
    Environments environment;
    
    if (runAsynchronously)
    {
        environment = await _context.Enivronments.Where(p => p.ObjektID == objectId).FirstOrDefaultAsync();
    }
    else
    {
        environment = await _context.Enivronments.Where(p => p.ObjektID == objectId).FirstOrDefault();
    }

    //check if env is already in database
    if (environment == null)
    {
        //create enve
        environment = new Environments()
        {
            Name = name,
            ObjektID = objectId,
            IsActive = false,
            Delay = 0,
            Sequence = 999
        };
        
        if (runAsynchronously)
        {
            await _context.Enivronments.AddAsync(environment);
        }
        else
        {
            _context.Enivronments.Add(environment);
        }
    }
    else
    {
        //Update env Name, if it has changed
        if (environment.Name != name)
            environment.Name = name;
    }
    
    return environment;
}

Alles bescheidene Lösungen. Ich würde einfach gar keine synchrone API anbieten.
 
Die Antwort von Stephen Cleary im Stackverflow Thread würde ich bei so One Linern bevorzugen.
Bei grösseren Methoden finde ich den async Wrapper für eine sync Methode durchaus mal angebracht.
Gibt aber sicher genug Leute die das anders sehen.
 
Danke für Eure Antworten.
Eigentlich würde ich gerne nur mit den async Methoden arbeiten, welche mir EFCore anbietet. Die Sache hat allerdings einen Haken. Obwohl mir EFCore Async-Methoden anbietet, scheint es nicht ThreadSafe zu sein. Nutze ich async und der User klickt im Interface herum und löst eine weitere Methode aus, welche auf den dbContext zugreift, dann bekomme ich eine Exception welche besagt, das nicht zwei Threads gleichzeitig den dbContext nutzen dürfen. Zugegeben, der User muss schon sehr schnell hin und herklicken. Aber das sollte eigentlich keine Rolle spielen. In meinem Fall startet die Anwendung (ASP Net Core Blazor) mit dem Laden des Dahsboards. Interessiert den User das nicht dann klickt er auf die gewünschte andere Seite und löst weitere Abfragen am DBContext aus. In wenigen Fällen bekomme ich dann die Exception.
Deswegen habe ich mich manchmal dazu entschlossen, die synchrone Methoden aufzurufen um a) die Exception zu verhindern, und b) den User daran zu hindern, wie Wild auf der UI rumzuklicken. Dann muss er halt mal bis zu 500ms warten.

Bis ich eine optimale Lösung für mein EFcore Problem gefunden habe, werde ich mir einer der o.g. Lösungsvorschläge bedienen.

Gruß Magic
 
Du könntest ja auch deinen Code thread-safe machen, indem du darin nur dann die kritschen Methoden vom EF aufrufst, wenn gerade kein anderer Codeteil damit aktiv ist. Bei einer Kollision gibt man entweder einen entsprechenden Fehler an den Aufrufer zurück oder die async Methode pausiert einfach, bis sie dran ist.
 
Kann es sein, dass dein DbContext ein Singleton ist? Verwendest du DI (solltest du, wenn nicht)? Dort deinen DbContext mal Transient registrieren, damit dein Service eine neue Instanz bekommt.
 
Zuletzt bearbeitet:
Magic1416 schrieb:
Nutze ich async und der User klickt im Interface herum und löst eine weitere Methode aus, welche auf den dbContext zugreift, dann bekomme ich eine Exception welche besagt, das nicht zwei Threads gleichzeitig den dbContext nutzen dürfen.
Ich hab leider keine Erfahrung mit GUI Anwendungen in C#, aber das ist genau das zu erwartende Verhalten von dem EF Core DbContext. In ASP.NET Webservern wird der DbContext über Dependency Injection als "Scoped" geladen, d.h. ein DbContext pro Request. In deinem Fall müsste der DbContext als "Transient" über Dependency Injection geladen werden damit jedes Mal ein neuer DbContext erstellt wird.

Die Idee mit dem DbContext ist das er das "unit of work" abbildet, und das man zusammenhängende Vorgänge auf diesem DbContext durchführt und dann z.B. SaveChangesAsync aufruft. Da können dann natürlich nicht mehrere Threads gleichzeitig auf dem DbContext rumbasteln.

Ganz generell ist Async die Zukunft bei C#, und Sync und Async Code vertragen sich nicht gut wenn sie voneinander aufgerufen werden müssen. Ich würde da wirklich die Zeit reinstecken das jetzt richtig zu verstehen, und nicht eine Notlösung basteln die später richtig Ärger macht.
 
@Dalek
@VD90

Der DBContext ist default, also Scoped im DI Container registriert, genau wie das IRepository. Ich war bisher abgeneigt, beides Transient zu setzen. Auf irgendeiner Webseite hieß es, dass das wohl auch nicht so toll sein soll. Muss ich nochmal nach dem Artikel suchen. Ich teste mal Transient und schau, wie es sich verhält. Wenn das klappt, dann verwerfe ich die synchronen Methoden.
 
Magic1416 schrieb:
@Dalek
@VD90

Der EFContext ist default, also Scoped im DI Container registriert, genau wie das IRepository. Ich war bisher abgeneigt, beides Transient zu setzen. Auf irgendeiner Webseite hieß es, dass das wohl auch nicht so toll sein soll. Muss ich nochmal nach dem Artikel suchen. Ich teste mal Transient und schau, wie es sich verhält. Wenn das klappt, dann verwerfe ich die synchronen Methoden.
Scoped ist richtig wenn man in einer Web API einzelne requests bearbeitet. Keine Ahnung wie das bei Blazor ist, evtl. kommt daher das Problem. Ich würde da evtl. auch mal explizit in Kombination mit Blazor nach dem Problem suchen, das ist eine ganz andere Situation als eine Web API mit klar definierten HTTP requests.
 
@Dalek
Ich habe jetzt DBContext sowie die IRepositorys als Transient registriert. Das hatte natürlich auf anderer Seite Konsequenzen, zb beim IRepositoryUser, welche die Schnittstelle für alle userbezogenen Einstellungen in der Datenbank bereitstellt. Das auf Transient gesetzt führt zu einen seltsamen Verhalten der Oberfläche. Daher bin ich zur folgenden Lösung übergegangen.
1. Jedes IRepository injected DBContext über den Konstruktor
2. DBContext ist Transient im Container registriert.
3. IRepositoryUser ist Scoped registriert, während IRepositoryEnvironment Transient registriert ist

Meine Erwartungshaltung:
Obwohl DBKontext Transient ist, habe ich für die IRepositoryUser Schnittstelle nur diesen einen gemeinsamen Kontext über alle Seiten hinweg. Hingegen bei IRepositoryEnvironment habe ich auf jeder Seite, auf denen das Interface verwendet wird einen eigenen Kontext und vermeide damit den gleichzeitigen Zugriff von Threads. Das scheint zumindest auf den ersten Blick zu funktionieren.

Die Frage ist, ob es legitim ist, einen Transient registrierten DBContext mit einem Scoped registrierten IRepository zu verwenden, welches DBContext injected. Oder hol ich mir da später wieder andere Probleme ins Haus.

Gruß Magic
 
Mit Blazor kenne ich mich nicht aus, mir ist einfach nicht klar wie der ganze Lifecycle da funktioniert mit EF Core. Das scheint aber eher so ein bißchen nervig zu sein, die folgende Seite von Microsoft erklärt da ein paar Sachen dazu:

https://docs.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-5.0

Generell sollte man DbContext nicht wiederverwenden, sondern für einen zusammenhängenden Satz an Operationen einen DbContext verwendet. Das gilt ganz besonders wenn man Sachen speichert, der Kontext merkt sich was man da so macht.
 
Hier wird auf Github darüber gesprochen, wie Blazor wohl am besten mit EF Core DBcontext umgehen soll. So wie ich das bisher verstanden habe, gibt es dafür in .Net5 auch eine neue Factory, die das etwas erleichtern soll.
 
Zurück
Oben