AsyncService-Implementierung

.NET bietet von Hause aus die Möglichkeit, das Thread-Management dahingehend zu optimieren, dass temporär Threads freigegeben werden können wenn auf andere teure (insbesondere externe) Operationen wie Datenbankzugriffe oder Serivce-Aufrufe durchgeführt werden. Somit kann man in Summe höhere Verarbeitungsgeschwindigkeiten erreichen, da beispielsweise während einer DB-Abfrage der Thread freigegeben und für andere Dinge genutzt werden kann.

Details hierzu finden sich beispielsweise in

Design

Um async/await in der Service-Implementierung nutzen zu können, sollten folgende Schritte ausgeführt werden.

  • In ComponentTests- und IntegrativeTests-Projekten muss in der app.config folgender Eintrag vorgenommen werden: <add key="wcf:disableOperationContextAsyncFlow" value="false" />.
  • In der Services-Projektdatei muss <EnableOperationContextAsyncFlow>true</EnableOperationContextAsyncFlow> für den PackageBuilder angegeben werden.
  • EnhancedServiceBehaviorInstaller im DI-Container anbinden
  • Das Attribut [OperationBehavior(TransactionScopeRequired = true)] in [CSOperationBehavior(TransactionScopeRequired = true)] umbenennen
  • Mit Hilfe von Sigento, den Code der Service-Schnittelle gemäß async/await-Muster generieren (Task<> als Rückgabewert, async als Postfix für den Methodennamen)
  • async/await wie gewohnt implementieren

Implementierung

Das folgende Beispiel beschreibt, wie async/await mit den Verbesserungen in der Service-Implementierung genutzt werden kann.

Einmalige vorbereitenden Maßnahmen

Zunächst muss das ComponentTests- und IntegrativeTests-Projekt vorbereitet werden, so dass das Problem mit dem OperationContext gelöst wird, bei dem dieser nicht mehr Thread-Static ist. Hierzu muss ja schlussendlich in der app.config folgender Eintrag entstehen:

<configuration>
  ...
  <appSettings>
    <add key="wcf:disableOperationContextAsyncFlow" value="false" />
    ...
  </appSettings>
  ...
</configuration>

Im Services-Projekt ist dies nicht notwendig, da das Deployment automatisch einen entsprechenden Eintrag in der web.config anlegt, wenn in einer PropertyGroup, welche nicht auf Debug oder Release eingeschränkt ist, folgender Eintrag für den Schleupen.CS.CreateServicesPackage-PackageBuilder vorhanden ist:

<PropertyGroup>
  ...
  <EnableOperationContextAsyncFlow>true</EnableOperationContextAsyncFlow>
</PropertyGroup>

Als nächstes muss der sogenannte EnhancedWcfServiceBehaviorInstaller in Castle angebunden werden, um die Funktionalität von async/await für WCF generell anzuschließen. Hierzu muss zunächst das Nuget-Paket Schleupen.CS.PI.Framework.Services.Core.Castle3 in der paket.dependencies ergänzt und in der paket.references der Services angeschlossen werden.

public class ServiceHostFactoryConfigurator : WcfContainerConfigurator
{
    ...
    protected override void OnContainerConfigured(IWindsorContainer windsorContainer)
    {
        windsorContainer.Install(new EnhancedWcfServiceBehaviorInstaller(IsInsideWcfOperation));
        ...
        base.OnContainerConfigured(windsorContainer);
    }
}
Eigentliche Implementierung

Die WSDL muss für Sigento in ein Verzeichnis namens AsyncServer gelegt werden.

Sigento generiert daraufhin asynchrone Methoden in der C#-Schnittstelle:

Durch Anschluss des Attributs CSOperationBehavior wird diese Operation in einen TransactionScope eingebettet. Bei erfolgreicher Verarbeitung wird ein Commit durchgeführt, bei einem Fehler - auch bei einem Serialisierungsfehler - wird ein Rollback durchgeführt.

[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
[ErrorHandlerBehavior(typeof(ValidationFaultAssembler), typeof(UnhandledFaultAssembler<UnhandledFaultContract>))]
public sealed class BuecherAusleihenActivityService : IBuecherAusleihenActivityService
{
    [CSOperationBehavior(TransactionScopeRequired = true)]
    public async Task<AusleihenResponse> AusleihenAsync(AusleihenRequest request)
    {
        ...
        AusleiheListe ausleihen = await buecherAusleihenController.LeiheAusAsync(
            authenticationProvider.SessionCreatorSid,
            buecher);
        AusleihenResponse response = new(
            ausleiheAssembler.ToDataContract(ausleihen.Items).ToList());

        return response;
    }
}
Anpassung ComponentTests

Bei Component Tests kann die synchron implementierte Schnittstelle (also ohne async) angebunden werden:

public void WithOpenedServiceHost(Action<IBuecherAusleihenActivityServiceClient> action)
{
    Mocks.BuecherAusgeliehenEventServiceClientFactoryStub
              .Setup(x => x.Create())
              .Returns(Mocks.BuecherAusgeliehenEventServiceClientStub.Object);
    Mocks.BuecherAusgeliehenEventServiceClientStub
              .Setup(x => x.RaisedAsync(It.IsAny<RaisedNotification>()))
              .ReturnsAsync(new RaisedResponse());
    ...
    base.WithOpenedServiceHost(action);
}

Moq, ab Version 4.16, bietet die Methode ReturnsAsync<> an. Damit lässt sich vermeiden, dass man das Rückgabeobjekt zusätzlich in einen Task wrappen muss.

Auswirkungen auf Unit-Tests

Es ist zu beachten, dass Test-Methoden für asynchrone Methoden ebenfalls asynchron sein müssen, d.h. den async-Modifizierer und einen System.Threading.Tasks.Task<>-Rückgabetyp aufweisen.

    [Test]
    public async Task LeiheAusAsync_WhereBenutzerkontoIsUnknown_ShouldThrowInvalidOperatonException()
    {
        fixture.Mocks.BuchRepositoryMock
               .Setup(x => x.QueryByIdAsync(fixture.TestData.BuchVerweise))
               .ReturnsAsync(new Buecher());
        fixture.Mocks.BenutzerkontoRepositoryMock
               .Setup(x => x.QueryByAsync(fixture.TestData.ExternalIdOfBenutzer))
               .ReturnsAsync((Benutzerkonto)null);

        AusleihAuftrag testInput = new(
            fixture.TestData.ExternalIdOfBenutzer,
            fixture.TestData.BuchVerweise,
            null,
            DateTimeOffset.Now);

        BuecherAusleihenController testObject = fixture.CreateTestObject();
        await testObject.Invoking(x => x.LeiheAusAsync(testInput))
                        .Should().ThrowAsync<ValidationException>();
    }
Abgrenzung
  • Dies Muster beschreibt nicht, wie im Detail async/await korrekt zu implementieren ist.
  • Die Implementierung bezieht sich nur auf Service-Code.
Anmerkungen

Ist das WCF-Attribut OperationBehavior an der Service-Operation angebunden, so wird das Originalverhalten von WCF beibehalten!

In diesem Muster (und dem damit zur Verfügung gestellten Framework-Code) werden folgende Probleme gelöst:

  • Implementiert man async/await im Service mit Transaktion, so muss der TransactionScope mit sogenannter TransactionScopeAsyncFlowOption.Enabled initialisiert werden (da ansonsten der TransactionScope im gleichen Thread geschlossen werden muss).
    Dies kann man in der WCF normalerweise nicht angeben und wird durch die Schleupen-Erweiterung gelöst. Manuell kann / könnte man dies wie folgt implementieren:
public async Task<AusleihenResponse> AusleihenAsync(AusleihenRequest request)
{
    if (request == null) { throw  ArgumentNullException((request)); }
  
    using (ITransactionScope transactionScope = transactionScopeFactory.CreateTransactionScope())
    {
        IEnumerable&lt;BuchId> buecher = buchAssembler.ToDomainObjectIds(request.Buecher);
        AusleiheListe ausleihen = await buecherAusleihenController.LeiheAusAsync(authenticationProvider.SessionCreatorSid, buecher);
        AusleihenResponse response = new AusleihenResponse(ausleiheAssembler.ToDataContract(ausleihen.Items).ToList());
    
        transactionScope.Complete();
    
        // WICHTIG: ist bei der Serialisierung des Response in WCF hier ein Fehler, kann kein Rollback mehr erfolgen; das löst unsere WCF-Erweiterung auch!
    
        return response;
    }
}
  • Das ist aus zwei Gründen nicht ausreichend: erstens muss dies manuell angebunden werden (nicht so schlimm) und zweitens erfolgt kein Rollback, wenn beispielsweise beim Serialisieren ein Pflichtfeld nicht gesetzt ist. Das ist nicht gut und wird durch Framework-Code gelöst.
  • Tritt ein Fehler bei der Serialisierung des Response auf, so liefert die WCF nicht mehr die Details nach außen (obwohl intern eine sinnvolle Ausnahme geworfen worden ist). Diese wird ins Diagnoseprotokoll geschrieben.
  • Der OperationContext funktioniert aus Kompatibilitätsgründen im Standard mit async/await nicht wie erwartet - siehe https://github.com/Microsoft/dotnet/issues/403. Um einen OperationContext zu erhalten, der async/await unterstützt, muss der Eintrag in der web.config erscheinen. Entsprechend ist dieser Eintrag auch in den ComponentTests notwendig!
Stärken und Schwächen

Die in diesem Abschnitt beschriebenen Stärken und Schwächen sollen eine Entscheidungshilfe an die Hand geben, wann dieses Muster eingesetzt werden kann.

Stärken
  • Wir haben eine Lösung!
Schwächen
  • Fehler im Rahmen der Serialisierung des Ergebnisses der asynchronen Operation werden unspezifisch an den Client zurückgegeben. Die eigentliche Fehlermeldung findet sich lediglich im Diagnoseprotokoll. (Dies ist ein allgemeines WCF-Problem.)
  • Proprietäre Implementierung
Alternative Muster
  • Manuelle Implementierung wie oben mit den erwähnten Risiken (nicht zu empfehlen).
Migration
Version von synchronem zu asynchronem Code

Für die vorhandenen ältere Serviceversionen muss folgendes getan werden

  • Operationen desselben Service müssen ebenfalls auf async/await umgestellt werden
  • Die gesamte Kette von Fassade bis zu Repository und Gateways muss auf async/await umgestellt werden
  • Da bspw. das Repository auch in anderen Services verwendet wird, muss zunächst die async-Variante daneben implementiert werden
  • bei der Verwendung von DomainEvents müssen die Handler ggf. synchron d.h. ohne async implementiert werden

Zudem

  • Verwendung des Attributes CSOperationBehavior anstelle von OperationBehavior:
[CSOperationBehavior(TransactionScopeRequired = true)]
public AResponse A(ARequest)
  • Enfernung von TransactionIsolationLevel = IsolationLevel.ReadCommitted aus dem Attribut ServiceBehavior:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] 

Wird TransactionIsolationLevel = * nicht entfernt, wirft die WCF einen Laufzeitfehler, da kein OperationBehaviorAttribute konfiguriert ist! Das Verhalten ist richtig so, da die Transaktion nicht mehr über WCF sondern über Schleupen.CS konfiguriert wird.

Cookie Consent mit Real Cookie Banner