Pregunta

Estoy intentando escribir una clase que encapsula las llamadas de WCF (el cliente es Silverlight, si es importante). Todo funciona a la perfección, pero no estoy seguro de cómo interceptar un error de conexión, como si el servidor no respondiera. Parece que ese poco de trabajo ocurre en algún lugar del código resultante de ChannelFactory, pero no estoy seguro. La revisión del código general es bienvenida también. :)

La línea inferior, que rodea la creación del canal, o el delegado de resultados de inicio o asíncrono en try / catch no atrapa una conexión fallida. Me gustaría que esa captura ejecute el 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();
    }
}

Aquí está el seguimiento de la pila, para aquellos que tienen curiosidad:

 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)

Para que esto suceda, enciendo la aplicación en el navegador y luego finalizo el proceso del servidor web desde Visual Studio. En un entorno de prueba, obtengo lo mismo al eliminar la conexión de red del sistema cliente.

Aquí está la ToString () de la excepción COMPLETA:

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)

Por cierto, la única forma en que puedo detectar esto es mediante el uso del evento UnhandledException a nivel de aplicación en el cliente de SL.

¿Fue útil?

Solución

Jeff, no estoy completamente seguro de tu pregunta de cómo estás simulando un fallo de conexión. Supongo que " como si el servidor no respondiera " significa que estás buscando algún tipo de error de tiempo de espera. Probablemente esté simulando un servidor que no responde, forzando una respuesta lenta de su llamada de servicio usando Thread.Sleep () en el lado del servidor. ¿Derecha? ¿Lo suficientemente cerca?

Tuve un proyecto similar, así que probé esto y pude capturar un tiempo de espera y otras excepciones con éxito. Creo que quizás no viste estas excepciones porque tu enlace personalizado tuvo un tiempo de espera predeterminado muy largo y es posible que no hayas esperado el tiempo suficiente.

Para explicar, los tiempos de espera de WCF están controlados por la configuración de enlace. Mirando su código, está creando su propio enlace personalizado como este:

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

Si establece un punto de interrupción allí e inspecciona el enlace personalizado que creó, verá que tiene un minuto SendTimeout de forma predeterminada. Sospecho que no esperó lo suficiente o que no configuró el tiempo de espera simulado de su servicio lo suficiente, por lo que no pudo detectar una excepción de tiempo de espera.

Aquí hay algunos cambios de código para demostrar. Primero, para establecer el tiempo de espera en el enlace:

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

Finalmente, para detectar las excepciones y generar los eventos correctos, puedes actualizar tu delegado de AsyncCallback de la siguiente manera. La excepción se produce cuando se llama a 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 cuanto a la revisión general del código, le recomendaría que evite la codificación de su configuración de enlace. En su lugar, puede declarar la configuración de su punto final (incluidos los tiempos de espera razonables) en su archivo ServiceReferences.ClientConfig y agregar su fábrica de canales con el nombre del punto final como este:

new ChannelFactory<IFeedService>("feedServiceEndpoint");

Eso debería hacer que la aplicación sea más fácil de mantener con el tiempo.

Espero que esto ayude.

Jerry

Actualizado:

Jeff,

Por favor, mire este código y los comentarios para obtener una explicación más detallada de lo que está sucediendo aquí. El método MakeCall () está creando una función (delegado) que se pasa al método BeginGet (). Esa función se ejecuta más tarde cuando el servicio responde.

Realice los cambios sugeridos y establezca puntos de interrupción en las líneas que comienzan con " AsyncCallback asyncCallback " y " elementos var " ;. Verá que el primer paso a través del depurador simplemente declara que el código (función / delegado) se ejecuta cuando el servicio responde y el segundo paso es el procesamiento real de esa respuesta, comenzando en el interior de su declaración de delegado. Esto significa que el intento / captura externo no estará dentro del alcance cuando se procese la respuesta de la llamada de servicio.

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

Otros consejos

He encontrado un problema similar antes. El problema principal es la forma en que IDisposable se implementa en las instancias de su proxy / canal. La forma en que lo he resuelto se muestra en el código a continuación, donde IDirector es mi contrato de servicio:

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

La forma de usar la clase anterior es la siguiente:

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

Debido a que la clase ProxyWrapper implementa IDisposable , si usamos una instancia de la clase ProxyWrapper dentro de un usando bloque, se garantiza que el método Dispose () se llamará, incluso si se lanza una excepción. El código en el método Dispose () manejará todos los casos y estados del proxy / canal. Luego puede agregar su código de manejo de errores / registro / evento delegado en este método.

Lea la siguiente entrada del blog para obtener más información y para una versión más genérica del código anterior: http://bloggingabout.net/blogs/erwyn/archive/2006/12/09/WCF-Service-Proxy-Helper.aspx

Para detectar errores de comunicación con el servidor, sugeriría poner la llamada a .CreateChannel () en un intento ... capturar y manejar cosas como CommunicationException y TimeoutExceptions allí.

También, uno de los consejos generales más importantes: crear ChannelFactory es una operación bastante costosa, por lo que es posible que desee hacerlo por separado y almacenar esa referencia al objeto ChannelFactory en algún lugar.

Si necesita crear y posiblemente volver a crear el canal desde ChannelFactory, puede usar esa instancia almacenada / almacenada en caché de ChannelFactory una y otra vez sin tener que crearla todo el tiempo.

Pero aparte de eso, ¡creo que lo estás haciendo muy bien aquí!

Marc

Su instancia de ChannelFactory estará en " Anulada " Estado cuando se produce este tiempo de espera. Encontrará que la excepción se está produciendo no cuando ocurre la excepción en la llamada, sino cuando llama a _channel.Dispose ().

Tendrá que protegerse contra esta excepción de alguna manera. La forma más sencilla sería poner un try / catch alrededor del contenido de su método de disposición, pero sería mejor si verificara el estado de la instancia de _channel antes de intentar cerrar / eliminar.

Esta es la razón por la que aún está recibiendo la excepción, no se está manejando aquí.

Actualización :

Según el seguimiento de tu pila, parece que el marco está intentando ejecutar tu delegado de devolución de llamada y está obteniendo una excepción allí. Respondió en otra parte que intentar / capturar el contenido de su delegado no resuelve el problema ... ¿qué pasa con las excepciones internas? ¿Cuál es la salida completa de TheException.ToString ()?

Recorrí el .NET Framework y, de hecho, está permitido viajar todo el camino de regreso al conjunto de subprocesos (al menos en el seguimiento de apilamiento que tienes aquí), lo que causaría una excepción no controlada y tu subproceso morirá. En el peor de los casos, siempre puede asumir el hilo ... haga girar su propio hilo y llame al servicio de manera síncrona, en lugar de usar el patrón asíncrono integrado (a menos que se trate de una comunicación tipo WCF dúplex, que no utilizo). t piensa que es).

Todavía creo que la información completa de la excepción (si hay algo más) podría ser útil.

Jeff, muéstranos tu código cuando intentes poner un bloque try / catch alrededor de la llamada EndGet. Es difícil creer que no haya una excepción, y verlo me ayudará a creer.

También, como parte de este experimento, deshacerse del controlador de eventos UnhandledException. Creo que está detectando esta excepción tan pronto como el BrowserHttpRequest se da cuenta de que hay un 404. Creo que sin el evento UnhandledException, un try / catch alrededor de EndGet detectará la excepción, en su caso. Creo que estás captando un estado intermedio del proceso de manejo de excepciones.

También me gustaría que pongas un System.Debugger.Break o algo inmediatamente después de la llamada EndGet.

¿Estoy en lo cierto al pensar que ha diseñado su clase ServiceCaller para poder llamarla en un usando ?

Si es así, entonces ese es el problema. Las clases de canales WCF se han diseñado de tal manera que hacen que sea extremadamente difícil escribir un código de eliminación completamente general que satisfaga todas las posibles condiciones de error. Si fuera simple, entonces las clases de canal en sí serían seguras de eliminar y no necesitarías una clase envoltura.

La única forma segura que he encontrado hasta la fecha es encapsular no la lógica de disposición, sino toda la secuencia de llamadas, por lo tanto:

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

Sé que es una llamada síncrona, pero el principio es el mismo. En lugar de que intentes deducir si debes llamar a Close o Abort , lo determinas por adelantado según la distancia a la que llegaste a través de la llamada antes de que cayera. / p>

Nota: solo el canal real necesita este tratamiento especial. Puede deshacerse de la fábrica de la forma que desee AFAIK.

Creo que el problema está en la forma en que ha diseñado la persona que llama al servicio.

El canal se abre cuando se crea el llamante de servicio. Esto significa que el canal puede expirar y no hay nada en su código que pueda recuperarse de la cantidad de tiempo.

Movería la creación y el cierre del canal al método make call.

Puede hacer un intento de captura alrededor del inicio y alrededor del contenido de la devolución de llamada.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top