Как лучше всего обойти проблему с клиентом WCF, использующим блокировку? - PullRequest
393 голосов
/ 22 февраля 2009

Мне нравится создавать экземпляры моих клиентов службы WCF в блоке using, поскольку это в значительной степени стандартный способ использования ресурсов, реализующих IDisposable:

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

Но, как отмечалось в этой статье MSDN , упаковка клиента WCF в блок using может маскировать любые ошибки, которые приводят к тому, что клиент остается в неисправном состоянии (например, тайм-аут или проблема со связью). ). Короче говоря, когда вызывается Dispose (), клиентский метод Close () запускается, но выдает ошибку, потому что он находится в неисправном состоянии. Исходное исключение затем маскируется вторым исключением. Не хорошо.

Предложенный обходной путь в статье MSDN состоит в том, чтобы полностью избежать использования блока using, а вместо этого создавать экземпляры ваших клиентов и использовать их примерно так:

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

По сравнению с блоком using, я думаю, это ужасно. И много кода, чтобы писать каждый раз, когда вам нужен клиент.

К счастью, я нашел несколько других обходных путей, таких как этот, в IServiceOriented. Вы начинаете с:

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

Что позволяет:

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

Это неплохо, но я не думаю, что это так выразительно и легко понятно, как блок using.

Обходной путь, который я сейчас пытаюсь использовать, о котором я впервые прочитал на blog.davidbarret.net . По сути, вы переопределяете метод клиента Dispose() везде, где вы его используете. Что-то вроде:

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

Похоже, что можно снова разрешить блок using без опасности замаскировать исключение состояния ошибки.

Итак, есть ли какие-то другие ошибки, которые я должен обратить внимание на использование этих обходных путей? Кто-нибудь придумал что-нибудь лучше?

Ответы [ 26 ]

130 голосов
/ 22 февраля 2009

На самом деле, хотя я веду блог (см. ответ Люка ), я думаю эта лучше, чем моя IDisposable оболочка. Типичный код:

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

(редактировать по комментариям)

Поскольку Use возвращает void, самый простой способ обработки возвращаемых значений - через захваченную переменную:

int newOrderId = 0; // need a value for definite assignment
Service<IOrderService>.Use(orderService=>
  {
    newOrderId = orderService.PlaceOrder(request);
  });
Console.WriteLine(newOrderId); // should be updated
86 голосов
/ 15 сентября 2009

Учитывая выбор между решением, поддерживаемым IServiceOriented.com, и решением, поддерживаемым блогом Дэвида Баррета , я предпочитаю простоту, предложенную путем переопределения клиентского метода Dispose (). Это позволяет мне продолжать использовать оператор using (), как можно ожидать с одноразовым объектом. Однако, как указал @Brian, это решение содержит условие состязания, состоящее в том, что состояние может не быть сбойным при проверке, но может быть к моменту вызова Close (), и в этом случае CommunicationException по-прежнему возникает.

Итак, чтобы обойти это, я использовал решение, которое сочетает в себе лучшее из обоих миров.

void IDisposable.Dispose()
{
    bool success = false;
    try 
    {
        if (State != CommunicationState.Faulted) 
        {
            Close();
            success = true;
        }
    } 
    finally 
    {
        if (!success) 
            Abort();
    }
}
30 голосов
/ 22 февраля 2009

Я написал функцию высшего порядка , чтобы она работала правильно. Мы использовали это в нескольких проектах, и это, кажется, работает отлично. Вот как все должно было быть сделано с самого начала, без парадигмы «использования» и т. Д.

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

Вы можете звонить так:

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

Это почти так же, как и в вашем примере. В некоторых проектах мы пишем строго типизированные вспомогательные методы, поэтому в итоге мы пишем такие вещи, как «Wcf.UseFooService (f => f ...)».

Я считаю это довольно элегантным, учитывая все обстоятельства. С какой конкретной проблемой вы столкнулись?

Это позволяет подключать другие изящные функции. Например, на одном сайте сайт аутентифицируется в службе от имени вошедшего в систему пользователя. (Сайт сам по себе не имеет учетных данных.) Написав собственный помощник метода «UseService», мы можем настроить фабрику каналов так, как хотим, и т. Д. Мы также не обязаны использовать сгенерированные прокси - любой интерфейс подойдет .

27 голосов
/ 19 февраля 2011

Это рекомендуемый Microsoft способ обработки вызовов клиента WCF:

Подробнее см .: Ожидаемые исключения

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

Дополнительная информация Похоже, что так много людей задают этот вопрос на WCF, что Microsoft даже создала специальный образец, чтобы продемонстрировать, как обрабатывать исключения:

C: \ WF_WCF_Samples \ WCF \ Basic \ Client \ ExpectedExceptions \ CS \ клиент

Скачать образец: C # или VB

Учитывая, что существует очень много вопросов , связанных с оператором использования , (с подогревом?) Внутренние обсуждения и потоков по этому вопросу, я не собираюсь тратить свое время на попытки стать ковбоем кода и найти более чистый путь. Я просто смирюсь с этим и внедряю клиентов WCF таким подробным (но доверенным) способом для моих серверных приложений.

Необязательные дополнительные ошибки при отлове

Многие исключения происходят из CommunicationException, и я не думаю, что большинство из этих исключений следует повторить. Я пролистал каждое исключение в MSDN и нашел короткий список повторяющихся исключений (в дополнение к TimeOutException выше). Сообщите мне, если я пропустил исключение, которое следует повторить.

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

По общему признанию, это немного мирского кода для записи. В настоящее время я предпочитаю этот ответ и не вижу в этом коде "хаков", которые могут вызвать проблемы в будущем.

14 голосов
/ 22 января 2010

Наконец-то я нашел несколько серьезных шагов к чистому решению этой проблемы.

Этот пользовательский инструмент расширяет WCFProxyGenerator для предоставления прокси обработки исключений. Он генерирует дополнительный прокси с именем ExceptionHandlingProxy<T>, который наследует ExceptionHandlingProxyBase<T> - последний из которых реализует основные функции прокси. В результате вы можете использовать прокси-сервер по умолчанию, который наследует ClientBase<T> или ExceptionHandlingProxy<T>, который инкапсулирует управление временем жизни фабрики каналов и канала. ExceptionHandlingProxy учитывает ваш выбор в диалоговом окне «Добавить ссылку на службу» в отношении асинхронных методов и типов коллекций.

Codeplex имеет проект под названием Обработка исключений Генератор прокси WCF . По сути, он устанавливает новый пользовательский инструмент в Visual Studio 2008, а затем использует этот инструмент для создания нового прокси службы (Добавить ссылку на службу) . Он имеет некоторые хорошие функции для работы с неисправными каналами, тайм-аутами и безопасным удалением. Здесь есть отличное видео под названием ExceptionHandlingProxyWrapper , объясняющее, как именно это работает.

Вы можете снова безопасно использовать оператор Using, и, если в каком-либо запросе произошел сбой канала (TimeoutException или CommunicationException), Оболочка повторно инициализирует сбойный канал и повторно выполнит запрос. Если это не удастся, он вызовет команду Abort(), утилизирует прокси и сбросит исключение. Если служба сгенерирует код FaultException, она прекратит выполнение, и прокси-сервер будет безопасно прерван, что приведет к правильному исключению, как и ожидалось.

10 голосов
/ 25 августа 2012

Основываясь на ответах Марка Гравелла, MichaelGG и Мэтта Дэвиса, наши разработчики придумали следующее:

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

Пример использования:

string result = string.Empty;

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

Это максимально приближенный к синтаксису "using", вам не нужно возвращать фиктивное значение при вызове метода void, и вы можете сделать несколько вызовов сервиса (и вернуть несколько значений) без необходимости использовать кортежи.

Кроме того, при желании вы можете использовать это с ClientBase<T> потомками вместо ChannelFactory.

Метод расширения доступен, если разработчик хочет вместо него вручную использовать прокси / канал.

8 голосов
/ 02 мая 2013

@ Марк Гравелл

Не было бы нормально использовать это:

public static TResult Using<T, TResult>(this T client, Func<T, TResult> work)
        where T : ICommunicationObject
{
    try
    {
        var result = work(client);

        client.Close();

        return result;
    }
    catch (Exception e)
    {
        client.Abort();

        throw;
    }
}

Или то же самое (Func<T, TResult>) в случае Service<IOrderService>.Use

Это упростит возврат переменных.

7 голосов
/ 21 февраля 2012

Что это?

Это версия принятого ответа CW, но с (что я считаю завершенным) обработкой исключений.

Принятый ответ ссылается на этот веб-сайт, которого больше нет . Чтобы избавить вас от неприятностей, я включил самые важные части здесь. Кроме того, я немного изменил его, чтобы включить обработку повторных попыток исключения для обработки этих надоедливых сетевых таймаутов.

Простое использование клиента WCF

После создания прокси на стороне клиента, это все, что вам нужно для его реализации.

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

ServiceDelegate.cs

Добавьте этот файл к вашему решению. Никаких изменений в этом файле не требуется, если только вы не хотите изменить количество повторных попыток или какие исключения вы хотите обработать.

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;


       Exception mostRecentEx = null;
       int millsecondsToSleep = 1000;

       for(int i=0; i<5; i++)  // Attempt a maximum of 5 times 
       {
           try
           {
               codeBlock((T)proxy);
               proxy.Close();
               success = true; 
               break;
           }

           // The following is typically thrown on the client when a channel is terminated due to the server closing the connection.
           catch (ChannelTerminatedException cte)
           {
              mostRecentEx = cte;
               proxy.Abort();
               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep  * (i + 1)); 
           }

           // 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)
           {
              mostRecentEx = enfe;
               proxy.Abort();
               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }

           // The following exception that is thrown when a server is too busy to accept a message.
           catch (ServerTooBusyException stbe)
           {
              mostRecentEx = stbe;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }
           catch (TimeoutException timeoutEx)
           {
               mostRecentEx = timeoutEx;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           } 
           catch (CommunicationException comException)
           {
               mostRecentEx = comException;
               proxy.Abort();

               //  delay (backoff) and retry 
               Thread.Sleep(millsecondsToSleep * (i + 1)); 
           }
           catch(Exception )
           {
                // rethrow any other exception not defined here
                // You may want to define a custom Exception class to pass information such as failure count, and failure type
                proxy.Abort();
                throw ;  
           }
       }
       if (success == false && mostRecentEx != null) 
       { 
           proxy.Abort();
           throw new Exception("WCF call failed after 5 retries.", mostRecentEx );
       }

    }
}

PS: я сделал этот пост вики-сообществом. Я не буду собирать «баллы» из этого ответа, но предпочитаю, чтобы вы проголосовали за него, если вы согласны с реализацией, или измените его, чтобы сделать его лучше.

7 голосов
/ 30 августа 2011

Ниже приведена расширенная версия источника из вопроса , расширенная для кэширования нескольких канальных фабрик и попытки поиска конечной точки в файле конфигурации по имени контракта.

Используется .NET 4 (в частности: контрастность, LINQ, var):

/// <summary>
/// Delegate type of the service method to perform.
/// </summary>
/// <param name="proxy">The service proxy.</param>
/// <typeparam name="T">The type of service to use.</typeparam>
internal delegate void UseServiceDelegate<in T>(T proxy);

/// <summary>
/// Wraps using a WCF service.
/// </summary>
/// <typeparam name="T">The type of service to use.</typeparam>
internal static class Service<T>
{
    /// <summary>
    /// A dictionary to hold looked-up endpoint names.
    /// </summary>
    private static readonly IDictionary<Type, string> cachedEndpointNames = new Dictionary<Type, string>();

    /// <summary>
    /// A dictionary to hold created channel factories.
    /// </summary>
    private static readonly IDictionary<string, ChannelFactory<T>> cachedFactories =
        new Dictionary<string, ChannelFactory<T>>();

    /// <summary>
    /// Uses the specified code block.
    /// </summary>
    /// <param name="codeBlock">The code block.</param>
    internal static void Use(UseServiceDelegate<T> codeBlock)
    {
        var factory = GetChannelFactory();
        var proxy = (IClientChannel)factory.CreateChannel();
        var success = false;

        try
        {
            using (proxy)
            {
                codeBlock((T)proxy);
            }

            success = true;
        }
        finally
        {
            if (!success)
            {
                proxy.Abort();
            }
        }
    }

    /// <summary>
    /// Gets the channel factory.
    /// </summary>
    /// <returns>The channel factory.</returns>
    private static ChannelFactory<T> GetChannelFactory()
    {
        lock (cachedFactories)
        {
            var endpointName = GetEndpointName();

            if (cachedFactories.ContainsKey(endpointName))
            {
                return cachedFactories[endpointName];
            }

            var factory = new ChannelFactory<T>(endpointName);

            cachedFactories.Add(endpointName, factory);
            return factory;
        }
    }

    /// <summary>
    /// Gets the name of the endpoint.
    /// </summary>
    /// <returns>The name of the endpoint.</returns>
    private static string GetEndpointName()
    {
        var type = typeof(T);
        var fullName = type.FullName;

        lock (cachedFactories)
        {
            if (cachedEndpointNames.ContainsKey(type))
            {
                return cachedEndpointNames[type];
            }

            var serviceModel = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).SectionGroups["system.serviceModel"] as ServiceModelSectionGroup;

            if ((serviceModel != null) && !string.IsNullOrEmpty(fullName))
            {
                foreach (var endpointName in serviceModel.Client.Endpoints.Cast<ChannelEndpointElement>().Where(endpoint => fullName.EndsWith(endpoint.Contract)).Select(endpoint => endpoint.Name))
                {
                    cachedEndpointNames.Add(type, endpointName);
                    return endpointName;
                }
            }
        }

        throw new InvalidOperationException("Could not find endpoint element for type '" + fullName + "' in the ServiceModel client configuration section. This might be because no configuration file was found for your application, or because no endpoint element matching this name could be found in the client element.");
    }
}
5 голосов
/ 07 декабря 2010

Оболочка, как это будет работать:

public class ServiceClientWrapper<ServiceType> : IDisposable
{
    private ServiceType _channel;
    public ServiceType Channel
    {
        get { return _channel; }
    }

    private static ChannelFactory<ServiceType> _channelFactory;

    public ServiceClientWrapper()
    {
        if(_channelFactory == null)
             // Given that the endpoint name is the same as FullName of contract.
            _channelFactory = new ChannelFactory<ServiceType>(typeof(T).FullName);
        _channel = _channelFactory.CreateChannel();
        ((IChannel)_channel).Open();
    }

    public void Dispose()
    {
        try
        {
            ((IChannel)_channel).Close();
        }
        catch (Exception e)
        {
            ((IChannel)_channel).Abort();
            // TODO: Insert logging
        }
    }
}

Это должно позволить вам написать код вроде:

ResponseType response = null;
using(var clientWrapper = new ServiceClientWrapper<IService>())
{
    var request = ...
    response = clientWrapper.Channel.MyServiceCall(request);
}
// Use your response object.

Оболочка, конечно, может перехватывать больше исключений, если это требуется, но принцип остается тем же.

...