Domanda

Sto provando a scrivere una classe che incapsula le chiamate WCF (il client è Silverlight, se è importante). Funziona a meraviglia, ma non sono sicuro di come intercettare un errore di connessione, come se il server non rispondesse. Sembrerebbe che un po 'di lavoro avvenga da qualche parte nel codice risultante da ChannelFactory, ma non ne sono sicuro. Anche la revisione del codice generale è benvenuta. :)

La linea di fondo, che circonda la creazione del canale, o il delegato del risultato iniziale o asincrono in try / catch non intrappola una connessione fallita. Vorrei che quell'attacco eseguisse l'evento 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();
    }
}

Ecco la traccia dello stack, per coloro che sono curiosi:

 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)

Per farlo, accendo l'app nel browser, quindi interrompo il processo del server Web da Visual Studio. In un ambiente di test, ottengo la stessa cosa uccidendo la connessione di rete per il sistema client.

Ecco l'eccezione COMPLETA ToString ():

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)

L'unico modo in cui riesco a capirlo è utilizzare l'evento UnhandledException a livello di app nel client SL.

È stato utile?

Soluzione

Jeff, non sono del tutto sicuro dalla tua domanda su come stai simulando un errore di connessione. Sto assumendo che "come se il server non rispondesse" significa che stai cercando un errore di timeout. Probabilmente stai simulando un server non responsive forzando una risposta lenta dalla chiamata di servizio utilizzando Thread.Sleep () sul lato server. Destra? Abbastanza vicino?

Avevo un progetto simile, quindi l'ho provato e sono riuscito a intercettare un timeout e altre eccezioni con successo. Penso che forse non stavi vedendo queste eccezioni perché la tua associazione personalizzata aveva un timeout predefinito molto lungo e potresti non aver atteso abbastanza a lungo.

Per spiegare, i timeout WCF sono controllati dalla configurazione dell'associazione. Guardando il tuo codice, stai creando la tua associazione personalizzata in questo modo:

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

Se si imposta un punto di interruzione lì e si controlla l'associazione personalizzata creata, si vedrà che ha un minuto SendTimeout per impostazione predefinita. Ho il sospetto che non stavi aspettando abbastanza a lungo o non hai impostato il timeout simulato del tuo servizio abbastanza a lungo, quindi non sei stato in grado di cogliere un'eccezione di timeout.

Ecco alcune modifiche al codice da dimostrare. Innanzitutto, per impostare il timeout sull'associazione:

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

Infine, per rilevare le eccezioni e generare gli eventi corretti, puoi aggiornare il tuo delegato AsyncCallback come segue. L'eccezione viene generata quando viene chiamato 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);
    }
};

Per quanto riguarda una revisione generale del codice, consiglierei di evitare di codificare a fondo la configurazione di associazione. È possibile invece dichiarare la configurazione dell'endpoint (compresi i timeout ragionevoli) nel file ServiceReferences.ClientConfig e rinnovare la produzione del canale con il nome dell'endpoint in questo modo:

new ChannelFactory<IFeedService>("feedServiceEndpoint");

Ciò dovrebbe rendere l'app più gestibile nel tempo.

Spero che questo aiuti.

Jerry

Aggiornamento:

Jeff,

Dai un'occhiata a questo codice e ai commenti all'interno per ulteriori spiegazioni su ciò che sta accadendo qui. Il metodo MakeCall () sta creando una funzione (delegato) che viene passata al metodo BeginGet (). Tale funzione viene successivamente eseguita quando il servizio risponde.

Apporta le modifiche suggerite e imposta i punti di interruzione nelle righe che iniziano con " AsyncCallback asyncCallback " e "articoli vari". Vedrai che il primo passaggio attraverso il debugger dichiara semplicemente che il codice (funzione / delegato) deve essere eseguito quando il servizio risponde e il secondo passaggio è l'effettiva elaborazione di tale risposta, a partire dall'interno della dichiarazione del delegato. Ciò significa che il try / catch esterno non sarà compreso nell'ambito dell'elaborazione della risposta alla chiamata di servizio.

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

Altri suggerimenti

Ho riscontrato un problema simile prima. Il problema principale è il modo in cui IDisposable è implementato su istanze del tuo proxy / canale. Il modo in cui l'ho risolto è mostrato nel codice seguente, dove IDirector è il mio contratto di servizio:

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;
        }
    }
}

Il modo di usare la classe sopra è il seguente:

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

Poiché la classe ProxyWrapper implementa IDisposable , se utilizziamo un'istanza della classe ProxyWrapper all'interno di un utilizzando blocco, il metodo Dispose () è garantito per essere chiamato, anche se viene generata un'eccezione. Il codice nel metodo Dispose () gestirà tutti i casi e gli stati del proxy / canale. È quindi possibile aggiungere il codice di gestione degli errori / registrazione / delegato di eventi in questo metodo.

Leggi il seguente post di blog per ulteriori informazioni e per una versione più generica del codice sopra: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

Per rilevare errori di comunicazione con il server, suggerirei di mettere la chiamata a .CreateChannel () in un tentativo ... intercetta e gestisci cose come CommunicationException e TimeoutExceptions lì.

Inoltre, uno dei più generici consigli generali: la creazione di ChannelFactory è un'operazione piuttosto costosa, quindi potresti voler fare questo separatamente e memorizzare quel riferimento all'oggetto ChannelFactory da qualche parte.

Se è necessario creare e possibilmente ricreare il canale da ChannelFactory, è possibile utilizzare più volte l'istanza memorizzata / memorizzata nella cache di ChannelFactory senza doverlo creare continuamente.

Ma a parte questo, penso che stai andando abbastanza bene qui!

Marc

L'istanza di ChannelFactory si troverà in " Aborted " indicare quando si verifica questo timeout. Scoprirai che l'eccezione viene generata non quando si verifica l'eccezione nella chiamata, ma quando chiami _channel.Dispose ().

Dovrai proteggerti da questa eccezione in qualche modo. Il modo più semplice sarebbe di provare / aggirare il contenuto del metodo dispose, ma sarebbe meglio se si controllasse lo stato dell'istanza _channel prima di provare a chiudere / disporre.

Questo è il motivo per cui stai ancora ricevendo l'eccezione: non viene gestita qui.

Aggiornamento:

In base alla traccia dello stack, sembra che il framework stia tentando di eseguire il delegato di callback e stia ottenendo un'eccezione lì. Hai risposto altrove che provare / catturare i contenuti del tuo delegato non risolve il problema ... che dire delle eccezioni interne? Qual è l'output completo di TheException.ToString ()?

Ho attraversato il .NET Framework ed è infatti consentito tornare indietro fino al threadpool (almeno nello stacktrace che hai qui) che provocherebbe la morte di un'eccezione non gestita e del thread. Se il peggio peggiora, puoi sempre assumere il threading ... fai girare il tuo thread e chiama il servizio in modo sincrono, anziché utilizzare il modello asincrono incorporato (a meno che non si tratti di un tipo di comunicazione WCF Duplex, che io non non credo che lo sia).

Penso ancora che le informazioni complete sull'eccezione (se c'è altro) potrebbero essere utili.

Jeff, ti preghiamo di mostrarci il tuo codice quando tenti di inserire un blocco try / catch attorno alla chiamata EndGet. È difficile credere che non cada un'eccezione e vederlo mi aiuterà a credere.

Inoltre, nell'ambito di questo esperimento, elimina il gestore di eventi UnhandledException. Penso che stai rilevando questa eccezione non appena BrowserHttpRequest si rende conto che c'è un 404. Penso che senza l'evento UnhandledException, un tentativo / catch intorno a EndGet catturerà l'eccezione, se presente. Penso che stai rilevando uno stato intermedio del processo di gestione delle eccezioni.

Mi piacerebbe anche vederti mettere un System.Debugger.Break o qualcosa subito dopo la chiamata EndGet.

Ho ragione nel pensare di aver progettato la tua classe ServiceCaller in modo da poterla chiamare in un blocco usando ?

Se sì, allora questo è il problema. Le classi di canali WCF sono state progettate in modo tale da rendere estremamente difficile la scrittura di un codice di smaltimento completamente generale che soddisfi tutte le possibili condizioni di errore. Se fosse semplice, le classi di canali stesse sarebbero sicure da smaltire e non avresti bisogno di una classe wrapper.

L'unico modo sicuro che ho trovato finora è incapsulare non la logica di smaltimento, ma l'intera sequenza di chiamate, quindi:

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();
        }
    }
}

So che è una chiamata sincrona, ma il principio è lo stesso. Invece di tentare di dedurre se è necessario chiamare Close o Abort , lo si determina in anticipo in base alla distanza che è riuscita a raggiungere prima che cadesse.

Nota: solo il canale effettivo richiede questo trattamento speciale. Puoi smaltire la fabbrica come preferisci AFAIK.

Penso che il problema sia nel modo in cui hai progettato il servizio di chiamata.

Il canale viene aperto quando viene creato il chiamante del servizio. Ciò significa che il canale può timeout e non c'è nulla nel codice che può recuperare dal timeount.

Sposterei la creazione e la chiusura del canale sul metodo make call.

Puoi provare a cercare il get start e il contenuto del callback.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top