Question

J'essaie d'écrire une classe qui encapsule les appels WCF (le client est Silverlight, si cela compte). Tout fonctionne à merveille, mais je ne sais pas trop comment intercepter une défaillance de connexion, comme si le serveur ne répondait pas. Il semblerait que quelque chose se passe quelque part dans le code résultant de ChannelFactory, mais je ne suis pas sûr. La révision générale du code est également bienvenue. :)

La ligne du bas, entourant la création du canal, ou le délégué de début ou de résultat async dans try / catch ne piège pas une connexion en échec. J'aimerais que cette capture exécute l'événement ServiceCallError.

public class ServiceCaller : IDisposable
{
    private IFeedService _channel;

    public ServiceCaller()
    {
        var elements = new List<BindingElement>();
        elements.Add(new BinaryMessageEncodingBindingElement());
        elements.Add(new HttpTransportBindingElement());
        var binding = new CustomBinding(elements);
        var endpointAddress = new EndpointAddress(App.GetRootUrl() + "Feed.svc");
        _channel = new ChannelFactory<IFeedService>(binding, endpointAddress).CreateChannel();
    }

    public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            var items = ((IFeedService)result.AsyncState).EndGet(result);
            if (ItemsRetrieved != null)
                ItemsRetrieved(this, new ServiceCallerEventArgs(items));
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }

    public event ItemsRetrievedEventHandler ItemsRetrieved;
    public event ServiceCallErrorHandler ServiceCallError;

    public delegate void ItemsRetrievedEventHandler(object sender, ServiceCallerEventArgs e);

    public delegate void ServiceCallErrorHandler(object sender, ServiceCallErrorEventArgs e);

    public void Dispose()
    {
        _channel.Close();
        _channel.Dispose();
    }
}

Voici la trace de la pile, pour ceux qui sont curieux:

 An AsyncCallback threw an exception.
at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

Pour ce faire, je lance l'application dans le navigateur, puis je tue le processus du serveur Web à partir de Visual Studio. Dans un environnement de test, j'obtiens la même chose en supprimant la connexion réseau du système client.

Voici ToString () de l'exception FULL:

System.Exception: An AsyncCallback threw an exception. ---> System.Exception: An AsyncCallback threw an exception. ---> System.ServiceModel.CommunicationException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound. ---> System.Net.WebException: The remote server returned an error: NotFound.
   at System.Net.Browser.BrowserHttpWebRequest.InternalEndGetResponse(IAsyncResult asyncResult)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClass5.<EndGetResponse>b__4(Object sendState)
   at System.Net.Browser.AsyncHelper.<>c__DisplayClass2.<BeginOnUI>b__0(Object sendState)
   --- End of inner exception stack trace ---
   at System.Net.Browser.AsyncHelper.BeginOnUI(SendOrPostCallback beginMethod, Object state)
   at System.Net.Browser.BrowserHttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.CompleteGetResponse(IAsyncResult result)
   --- End of inner exception stack trace ---
   at System.ServiceModel.Channels.Remoting.RealProxy.Invoke(Object[] args)
   at proxy_2.EndGet(IAsyncResult )
   at CoasterBuzz.Feed.Client.ServiceCaller.<MakeCall>b__0(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.CallComplete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.FinishSend(IAsyncResult result, Boolean completedSynchronously)
   at System.ServiceModel.Channels.ServiceChannel.SendAsyncResult.SendCallback(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   --- End of inner exception stack trace ---
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously, Exception exception)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelAsyncRequest.OnGetResponse(IAsyncResult result)
   at System.Net.Browser.BrowserHttpWebRequest.<>c__DisplayClassd.<InvokeGetResponseCallback>b__b(Object state2)
   at System.Threading._ThreadPoolWaitCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

En passant, la seule façon de comprendre cela est d'utiliser l'événement UnhandledException au niveau de l'application dans le client SL.

Était-ce utile?

La solution

Jeff, je ne suis pas tout à fait sûr de votre question de savoir comment vous simulez une défaillance de connexion. Je suppose que " comme si le serveur ne répondait pas " signifie que vous recherchez une sorte d'erreur de dépassement de délai. Vous simulez probablement un serveur non réactif en forçant une réponse lente de votre appel de service à l'aide de Thread.Sleep () côté serveur. Droite? Assez proche?

J'avais un projet similaire, je l'ai donc testé et j'ai réussi à intercepter un dépassement de délai d'attente et d'autres exceptions. Je pense que vous ne voyiez peut-être pas ces exceptions car votre liaison personnalisée avait un délai d'expiration par défaut très long et que vous n'aviez peut-être pas attendu assez longtemps.

Pour expliquer, les délais d'attente WCF sont contrôlés par la configuration de la liaison. En regardant votre code, vous créez votre propre liaison personnalisée, comme suit:

var elements = new List<BindingElement>();
elements.Add(new BinaryMessageEncodingBindingElement());
elements.Add(new HttpTransportBindingElement());
var binding = new CustomBinding(elements);

Si vous définissez un point d'arrêt à cet endroit et inspectez la liaison personnalisée que vous avez créée, vous verrez qu'il a une minute SendTimeout par défaut. Je suspecte que vous n'attendiez pas assez longtemps ou que vous n'ayez pas paramétré le délai d'attente simulé de votre service suffisamment longtemps. Par conséquent, vous n'avez pas pu intercepter une exception de délai d'attente.

Voici quelques modifications de code à démontrer. Tout d'abord, pour définir le délai d'expiration de la liaison:

var binding = new CustomBinding(elements);
//Set the timeout to something reasonable for your service
//This will fail very quickly
binding.SendTimeout = TimeSpan.FromSeconds(1);

Enfin, pour intercepter les exceptions et déclencher les événements appropriés, vous pouvez mettre à jour votre délégué AsyncCallback comme suit. L'exception est levée lors de l'appel de EndGet ().

AsyncCallback asyncCallBack = delegate(IAsyncResult result)
{
    IFeedService service = ((IFeedService)result.AsyncState);
    try
    {
        var items = service.EndGet(result);
        ItemsRetrieved(this, EventArgs.Empty);
    }
    catch (System.TimeoutException ex)
    {
        //Handles timeout
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.ServiceModel.CommunicationException ex)
    {
        //Handles a number of failures:
        //  Lack of cross-domain policy on target site
        //  Exception thrown by a service
        ServiceCallError(this, EventArgs.Empty);
    }
    catch (System.Exception ex)
    {
        //Handles any other errors here
        ServiceCallError(this, EventArgs.Empty);
    }
};

En ce qui concerne la révision générale du code, je vous recommande d'éviter de coder en dur votre configuration de liaison. Vous pouvez plutôt déclarer votre configuration de point de terminaison (y compris les délais raisonnables) dans votre fichier ServiceReferences.ClientConfig et créer une fabrique de canaux avec le nom du point de terminaison comme suit:

new ChannelFactory<IFeedService>("feedServiceEndpoint");

Cela devrait rendre l'application plus maintenable au fil du temps.

J'espère que cela vous aidera.

Jerry

Mis à jour:

Jeff,

Veuillez consulter ce code et les commentaires qu'il contient pour plus d'explications sur ce qui se passe ici. La méthode MakeCall () crée une fonction (délégué) qui est transmise à la méthode BeginGet (). Cette fonction est exécutée ultérieurement lorsque le service répond.

Veuillez apporter les modifications suggérées et définir des points d'arrêt sur les lignes commençant par "AsyncCallback asyncCallback". et "var items". Vous verrez que le premier passage dans le débogueur déclare simplement le code (fonction / délégué) à exécuter lorsque le service répond, et le second passage correspond au traitement réel de cette réponse, en commençant par l'intérieur de votre déclaration de délégué. Cela signifie que le try / catch externe ne sera pas dans la portée lorsque la réponse de l'appel de service sera traitée.

public void MakeCall(DateTime lastTime, Dictionary<string, string> context)
{
    try
    {
        AsyncCallback asyncCallBack = delegate(IAsyncResult result)
        {
            try
            {
                var items = ((IFeedService)result.AsyncState).EndGet(result);
                if (ItemsRetrieved != null)
                    ItemsRetrieved(this, new ServiceCallerEventArgs(items));
            }
            catch (Exception ex)
            { 
                //This will catch errors from the service call
            }
        };
        _channel.BeginGet(lastTime, context, asyncCallBack, _channel);
    }
    catch(Exception ex)
    {
        //This will not catch an error coming back from the service.  It will 
        //catch only errors in the act of calling the service asynchronously.

        //The communication with the service and response is handled on a different
        //thread, so this try/catch will be out of scope when that executes.

        //So, at this point in the execution, you have declared a delegate 
        //(actually an anonymous delegate the compiler will turn into a hidden class) 
        //which describes some code that will be executed when the service responds.

        //You can see this in action by setting a breakpoint just inside the delegate
        //at the line starting with "var items".  It will not be hit until the service
        // responds (or doesn't respond in a timeout situation).
    }
}

Jerry

Autres conseils

J'ai déjà rencontré un problème similaire. Le principal problème est lié à la manière dont IDisposable est implémenté sur les instances de votre proxy / canal. La façon dont j'ai résolu le problème est indiquée dans le code ci-dessous, où IDirector est mon contrat de service:

public class ProxyWrapper : IDisposable
{
    private IDirector proxy;
    private ChannelFactory<IDirector> factory;
    int callCount = 0;

    public ProxyWrapper()
    {
        factory = new ChannelFactory<IDirector>();

        proxy = factory.CreateChannel();
    }

    public IDirector Proxy
    {
        get
        {
            if (callCount > 0)
                throw new InvalidOperationException("The proxy can only be accessed once for every ProxyWrapper instance. You must create a new ProxyWrapper instance for each service request.");
            // only allow the proxy/channel to be used for one call.

            callCount++;
            return proxy;
        }
    }

    public void Dispose()
    {
        IClientChannel channel = (IClientChannel)proxy;

        try
        {
            if (channel.State != CommunicationState.Faulted)
            {
                channel.Close();
            }
            else
            {
                channel.Abort();
            }
        }
        catch (CommunicationException)
        {
            channel.Abort();
        }
        catch (TimeoutException)
        {
            channel.Abort();
        }
        catch (Exception)
        {
            channel.Abort();
            throw;
        }
        finally
        {
            channel = null;
            proxy = null;
        }
    }
}

Voici comment utiliser la classe ci-dessus:

    public static void Login(string userName, string password)
    {
        using (ProxyWrapper wrapper = new ProxyWrapper())
        {
            currentSession = wrapper.Proxy.Login(userName, password);
        }
    }

Étant donné que la classe ProxyWrapper implémente IDisposable , si nous utilisons une instance de la classe ProxyWrapper dans un à l'aide de Pour bloquer, la méthode Dispose () est appelée à être appelée, même si une exception est levée. Le code de la méthode Dispose () gérera tous les cas et états du proxy / canal. Vous pouvez ensuite ajouter votre code de délégué de gestion des erreurs / journalisation / événement dans cette méthode.

Lisez l'entrée de blog suivante pour plus d'informations et pour une version plus générique du code ci-dessus: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

Pour corriger les erreurs de communication avec le serveur, nous vous suggérons de placer l'appel à .CreateChannel () dans try ... intercepter et gérer des éléments tels que CommunicationException et TimeoutExceptions.

En outre, voici un petit conseil d'ordre général: la création de ChannelFactory est une opération assez coûteuse. Vous pouvez donc le faire séparément et stocker cette référence dans un objet ChannelFactory quelque part.

Si vous devez créer et éventuellement recréer le canal à partir de ChannelFactory, vous pouvez utiliser cette instance stockée / en cache de ChannelFactory sans avoir à le créer tout le temps.

Mais à part ça, je penserais que vous vous débrouillez bien ici!

Marc

Votre instance ChannelFactory sera dans le champ "Aborted". indiquer quand cette temporisation se produit. Vous constaterez que l'exception est levée non pas lorsque l'exception survient dans l'appel, mais lorsque vous appelez _channel.Dispose ().

Vous devrez vous prémunir contre cette exception. Le moyen le plus simple serait de mettre un try / catch autour du contenu de votre méthode de disposition, mais il serait préférable de vérifier l'état de l'instance _channel avant d'essayer de fermer / de disposer.

C’est la raison pour laquelle vous obtenez toujours l’exception - elle n’est pas gérée ici.

Mise à jour:

En fonction de votre trace de pile, il semblerait que la structure tente d'exécuter votre délégué de rappel et y génère une exception. Vous avez répondu ailleurs que d'essayer de capturer le contenu de votre délégué ne résout pas le problème ... qu'en est-il des exceptions internes? Quelle est la sortie complète de TheException.ToString ()?

J'ai parcouru le .NET Framework et il est en effet autorisé à remonter jusqu'au threadpool (au moins dans le stacktrace que vous avez ici), ce qui provoquerait la mort d'une exception non gérée et votre thread. Si le pire venait à se dégrader, vous pouvez toujours vous charger du fil ... créez votre propre fil et appelez le service de manière synchrone, plutôt que d'utiliser le modèle async intégré (à moins qu'il s'agisse d'une communication de type duplex WCF, ce que je ne fais pas. t pense que c'est).

Je pense toujours que les informations complètes sur l'exception (s'il y a quelque chose de plus) pourraient être utiles.

Jeff, affichez-nous votre code lorsque vous essayez de placer un bloc try / catch autour de l'appel EndGet. Il est difficile de croire que cela ne déclenche pas une exception, et le voir m'aidera à croire.

En outre, dans le cadre de cette expérience, supprimez le gestionnaire d'événements UnhandledException. Je pense que vous attrapez cette exception dès que BrowserHttpRequest se rend compte qu'il existe un 404. Je pense que sans l'événement UnhandledException, un try / catch autour de EndGet capturera l'exception, le cas échéant. Je pense que vous prenez un état intermédiaire du processus de traitement des exceptions.

J'aimerais aussi vous voir mettre un System.Debugger.Break ou quelque chose immédiatement après l'appel EndGet.

Ai-je raison de penser que vous avez conçu votre classe ServiceCaller afin de pouvoir l'appeler dans un bloc à l'aide de ?

Si oui, alors c'est le problème. Les classes de canaux WCF ont été conçues de manière à rendre extrêmement difficile l’écriture d’un code de disposition complètement général qui prenne en compte toutes les conditions d’erreur possibles. Si c’était simple, les classes de canaux elles-mêmes pourraient être éliminées en toute sécurité et vous n’auriez pas besoin d’une classe wrapper.

Le seul moyen sûr que j’ai trouvé à ce jour est d’encapsuler non pas la logique de disposition, mais la séquence d’appel complète, par exemple:

public static void Invoke<TContract>(ChannelFactory<TContract> factory, Action<TContract> action) where TContract : class {

    var proxy = (IClientChannel) factory.CreateChannel();
    bool success = false;
    try {
        action((TContract) proxy);
        proxy.Close();
        success = true;
    } finally {
        if(!success) {
            proxy.Abort();
        }
    }
}

Je sais que c'est un appel synchrone, mais le principe est le même. Au lieu d'essayer de déduire si vous devez appeler Fermer ou Abandonner , vous le déterminez à l'avance en fonction de la longueur de l'appel que vous avez réussi à obtenir avant de tomber.

Remarque: seul le canal actuel nécessite ce traitement spécial. Vous pouvez vous débarrasser de l'usine à votre guise, autant que je sache.

Je pense que le problème réside dans la manière dont vous avez conçu le service caller.

Le canal est ouvert lors de la création de l'appelant. Cela signifie que le canal peut expirer et qu'il n'y a rien dans votre code qui puisse récupérer du temps passé.

Je déplacerais la création et la fermeture du canal vers la méthode d’appel.

Vous pouvez faire un essai au début, à la lecture du contenu du rappel.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top