423 Stimmen

Was ist die beste Abhilfe für das WCF-Client "using" Block Problem?

Ich instanziere meine WCF-Dienst-Clients gerne innerhalb einer using Block, da dies so ziemlich die Standardmethode zur Verwendung von Ressourcen ist, die die IDisposable :

using (var client = new SomeWCFServiceClient()) 
{
    //Do something with the client 
}

Aber, wie bereits in dieser MSDN-Artikel einen WCF-Client in eine using Block könnte alle Fehler maskieren, die dazu führen, dass der Client in einem fehlerhaften Zustand verbleibt (wie eine Zeitüberschreitung oder ein Kommunikationsproblem). Langer Rede kurzer Sinn, wenn Dispose() aufgerufen wird, wird der Client Close() Methode wird ausgelöst, löst aber einen Fehler aus, weil sie sich in einem fehlerhaften Zustand befindet. Die ursprüngliche Ausnahme wird dann von der zweiten Ausnahme überdeckt. Das ist nicht gut.

Die im MSDN-Artikel vorgeschlagene Abhilfe besteht darin, die Verwendung einer using Block zu verwenden, und stattdessen Ihre Clients zu instanziieren und sie etwa so zu verwenden:

try
{
    ...
    client.Close();
}
catch (CommunicationException e)
{
    ...
    client.Abort();
}
catch (TimeoutException e)
{
    ...
    client.Abort();
}
catch (Exception e)
{
    ...
    client.Abort();
    throw;
}

Verglichen mit dem using Block, ich finde das hässlich. Und eine Menge Code zu schreiben, jedes Mal, wenn Sie einen Client benötigen.

Glücklicherweise habe ich einige andere Umgehungsmöglichkeiten gefunden, wie z. B. diese auf dem (jetzt nicht mehr existierenden) IServiceOriented Blog. Sie beginnen mit:

public delegate void UseServiceDelegate<T>(T proxy); 

public static class Service<T> 
{ 
    public static ChannelFactory<T> _channelFactory = new ChannelFactory<T>(""); 

    public static void Use(UseServiceDelegate<T> codeBlock) 
    { 
        IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel(); 
        bool success = false; 
        try 
        { 
            codeBlock((T)proxy); 
            proxy.Close(); 
            success = true; 
        } 
        finally 
        { 
            if (!success) 
            { 
                proxy.Abort(); 
            } 
        } 
     } 
} 

Das erlaubt dann:

Service<IOrderService>.Use(orderService => 
{ 
    orderService.PlaceOrder(request); 
}); 

Das ist nicht schlecht, aber ich denke nicht, dass es so ausdrucksstark und leicht verständlich ist wie die using Block.

Die Abhilfe, die ich derzeit versuche zu verwenden, habe ich zuerst auf blog.davidbarret.net . Im Grunde überschreiben Sie die Client Dispose() Methode, wo immer Sie sie verwenden. Etwa so:

public partial class SomeWCFServiceClient : IDisposable
{
    void IDisposable.Dispose() 
    {
        if (this.State == CommunicationState.Faulted) 
        {
            this.Abort();
        } 
        else 
        {
            this.Close();
        }
    }
}

Dies scheint es zu ermöglichen, die using wieder blockieren, ohne dass die Gefahr besteht, eine fehlerhafte Zustandsausnahme zu maskieren.

Gibt es noch andere Probleme, auf die ich bei der Verwendung dieser Workarounds achten muss? Ist jemandem etwas Besseres eingefallen?

92voto

Matt Davis Punkte 44077

Vor die Wahl gestellt zwischen der Lösung, die von IServiceOriented.com befürwortet wird, und der Lösung, die von David Barrets Blog Ich bevorzuge die Einfachheit, die sich aus der Überschreibung der Dispose()-Methode des Clients ergibt. Dadurch kann ich weiterhin die using()-Anweisung verwenden, wie man es bei einem Wegwerfobjekt erwarten würde. Wie @Brian jedoch feststellte, enthält diese Lösung eine Wettlaufbedingung, da der Status möglicherweise nicht fehlerhaft ist, wenn er überprüft wird, aber zum Zeitpunkt des Aufrufs von Close() fehlerhaft sein könnte, so dass die CommunicationException trotzdem auftritt.

Um dies zu umgehen, habe ich eine Lösung gefunden, die das Beste aus beiden Welten vereint.

void IDisposable.Dispose()
{
    bool success = false;
    try 
    {
        if (State != CommunicationState.Faulted) 
        {
            Close();
            success = true;
        }
    } 
    finally 
    {
        if (!success) 
            Abort();
    }
}

34voto

MichaelGG Punkte 9900

Ich schrieb eine höherwertige Funktion damit es richtig funktioniert. Wir haben dies in mehreren Projekten verwendet und es scheint gut zu funktionieren. So hätten die Dinge von Anfang an gemacht werden sollen, ohne das Paradigma des "Benutzens" und so weiter.

TReturn UseService<TChannel, TReturn>(Func<TChannel, TReturn> code)
{
    var chanFactory = GetCachedFactory<TChannel>();
    TChannel channel = chanFactory.CreateChannel();
    bool error = true;
    try {
        TReturn result = code(channel);
        ((IClientChannel)channel).Close();
        error = false;
        return result;
    }
    finally {
        if (error) {
            ((IClientChannel)channel).Abort();
        }
    }
}

Sie können auf diese Weise anrufen:

int a = 1;
int b = 2;
int sum = UseService((ICalculator calc) => calc.Add(a, b));
Console.WriteLine(sum);

Das ist ziemlich genau so wie in Ihrem Beispiel. In einigen Projekten schreiben wir stark typisierte Hilfsmethoden, so dass wir am Ende Dinge wie "Wcf.UseFooService(f=>f...)" schreiben.

Alles in allem finde ich es recht elegant. Gibt es ein bestimmtes Problem, auf das Sie gestoßen sind?

Dadurch lassen sich weitere raffinierte Funktionen einfügen. Auf einer Website beispielsweise authentifiziert sich die Website im Namen des angemeldeten Benutzers bei dem Dienst. (Die Site selbst hat keine Anmeldedaten.) Indem wir unsere eigene "UseService"-Methodenhilfe schreiben, können wir die Channel-Factory so konfigurieren, wie wir wollen, usw. Wir sind auch nicht an die Verwendung der generierten Proxys gebunden - jede Schnittstelle ist geeignet.

30voto

makerofthings7 Punkte 57238

Dies ist die von Microsoft empfohlene Art, WCF-Client-Aufrufe zu behandeln:

Für weitere Einzelheiten siehe: Erwartete Ausnahmen

try
{
    ...
    double result = client.Add(value1, value2);
    ...
    client.Close();
}
catch (TimeoutException exception)
{
    Console.WriteLine("Got {0}", exception.GetType());
    client.Abort();
}
catch (CommunicationException exception)
{
    Console.WriteLine("Got {0}", exception.GetType());
    client.Abort();
}

Zusätzliche Informationen So viele Leute scheinen diese Frage zu WCF zu stellen, dass Microsoft sogar ein spezielles Beispiel erstellt hat, um zu demonstrieren, wie man Ausnahmen behandelt:

c: \WF_WCF_Samples\WCF\Basic\Client\ExpectedExceptions\CS\client

Laden Sie das Muster herunter: C# oder VB

In Anbetracht der Tatsache, dass es so viele Probleme gibt unter Einbeziehung der using-Anweisung , (hitzige?) interne Diskussionen et Gewinde Ich werde meine Zeit nicht mit dem Versuch verschwenden, ein Code-Cowboy zu werden und einen saubereren Weg zu finden. Ich werde es einfach schlucken und WCF-Clients auf diese ausführliche (und dennoch vertrauenswürdige) Weise für meine Serveranwendungen implementieren.

Optionale zusätzliche zu fangende Ausfälle

Viele Ausnahmen ergeben sich aus CommunicationException und ich denke nicht, dass die meisten dieser Ausnahmen erneut geprüft werden sollten. Ich habe mich durch alle Ausnahmen auf MSDN gearbeitet und eine kurze Liste von Ausnahmen gefunden, die wiederholt werden können (zusätzlich zu TimeOutException oben). Bitte lassen Sie mich wissen, ob ich eine Ausnahme übersehen habe, die erneut versucht werden sollte.

  // The following is typically thrown on the client when a channel is terminated due to the server closing the connection.
catch (ChannelTerminatedException cte)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

// The following is thrown when a remote endpoint could not be found or reached.  The endpoint may not be found or 
// reachable because the remote endpoint is down, the remote endpoint is unreachable, or because the remote network is unreachable.
catch (EndpointNotFoundException enfe)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

// The following exception that is thrown when a server is too busy to accept a message.
catch (ServerTooBusyException stbe)
{
secureSecretService.Abort();
// todo: Implement delay (backoff) and retry
}

Zugegeben, das ist ein bisschen banaler Code, den man schreiben muss. Ich bevorzuge derzeit diese Antwort und ich sehe keine "Hacks" in diesem Code, die später Probleme verursachen könnten.

14voto

Neil Punkte 1902

Ich habe endlich ein paar solide Schritte zu einer sauberen Lösung für dieses Problem gefunden.

Dieses benutzerdefinierte Tool erweitert WCFProxyGenerator, um einen Proxy für die Ausnahmebehandlung bereitzustellen. Es erzeugt einen zusätzlichen Proxy namens ExceptionHandlingProxy<T> das erbt ExceptionHandlingProxyBase<T> - wobei letztere den Hauptteil der Funktionalität des Proxys implementiert. Das Ergebnis ist, dass Sie sich entscheiden können, den Standard-Proxy zu verwenden, der die ClientBase<T> o ExceptionHandlingProxy<T> die die Verwaltung der Lebensdauer der Kanalfabrik und des Kanals umfasst. ExceptionHandlingProxy beachtet Ihre Auswahl im Dialogfeld Dienstreferenz hinzufügen in Bezug auf asynchrone Methoden und Auflistungstypen.

Codeplex hat ein Projekt namens Ausnahmebehandlung WCF Proxy Generator . Es installiert im Grunde ein neues benutzerdefiniertes Tool in Visual Studio 2008 und verwendet dann dieses Tool, um den neuen Service-Proxy zu generieren (Dienstleistungsverweis hinzufügen) . Es bietet einige nützliche Funktionen für den Umgang mit fehlerhaften Kanälen, Zeitüberschreitungen und sicherer Entsorgung. Hier gibt es ein ausgezeichnetes Video mit dem Titel ExceptionHandlingProxyWrapper die genau erklären, wie das funktioniert.

Sie können sicher die Using Anweisung wiederholen, und wenn der Kanal bei einer Anforderung fehlerhaft ist (TimeoutException oder CommunicationException), wird der Wrapper den fehlerhaften Kanal neu initialisieren und die Abfrage erneut versuchen. Wenn dies fehlschlägt, ruft er die Abort() Befehl und entsorgen Sie den Proxy und werfen Sie die Exception zurück. Wenn der Dienst eine FaultException Code wird die Ausführung gestoppt, und der Proxy wird wie erwartet mit der richtigen Ausnahme abgebrochen.

11voto

TrueWill Punkte 24357

Auf der Grundlage der Antworten von Marc Gravell, MichaelGG und Matt Davis kamen unsere Entwickler zu den folgenden Ergebnissen:

public static class UsingServiceClient
{
    public static void Do<TClient>(TClient client, Action<TClient> execute)
        where TClient : class, ICommunicationObject
    {
        try
        {
            execute(client);
        }
        finally
        {
            client.DisposeSafely();
        }
    }

    public static void DisposeSafely(this ICommunicationObject client)
    {
        if (client == null)
        {
            return;
        }

        bool success = false;

        try
        {
            if (client.State != CommunicationState.Faulted)
            {
                client.Close();
                success = true;
            }
        }
        finally
        {
            if (!success)
            {
                client.Abort();
            }
        }
    }
}

Beispiel für die Verwendung:

string result = string.Empty;

UsingServiceClient.Do(
    new MyServiceClient(),
    client =>
    result = client.GetServiceResult(parameters));

Sie kommt der "using"-Syntax so nahe wie möglich, Sie müssen keinen Dummy-Wert zurückgeben, wenn Sie eine ungültige Methode aufrufen, und Sie können den Dienst mehrfach aufrufen (und mehrere Werte zurückgeben), ohne Tupel verwenden zu müssen.

Sie können dies auch mit ClientBase<T> Nachkommen anstelle von ChannelFactory, falls gewünscht.

Die Erweiterungsmethode wird offengelegt, wenn ein Entwickler stattdessen einen Proxy/Kanal manuell entsorgen möchte.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X