2 июля 2017 г.

WCF service. С чего начать. Основные моменты создание службы.

Примеры, которые используются тут, можно скачать по ссылке:

https://bitbucket.org/sergey_vaulin/wcf-examples/src

Еще ссылки:

  1. WCF service. С чего начать. Основные моменты создание службы. 
  2. WCF. Callback через BasicHttpBinding без дуплексной связи.
  3. Realtime WCF Tracker вызовов клиентского приложения.
  4. Как отключить публикацию метаданных mex WCF сервисом.  

Оглавление статьи:

  1. Что такое endpoint?
  2.  Способы общения клиента с сервисом. 
  3. Канал связи клиента и сервера.  
  4. Надежные каналы связи (reliableSession).
  5. Управление состоянием службы.
  6. Управление созданием экземпляров службы.
  7. Управление временем жизни экземпляра службы атрибутами контракта.
  8. Режимы параллельной работы с экземплярами службы
  9. Регулирование нагрузки.
  10. Обработка исключений.  
  11. Обработка исключений. Продолжение работы с посредником если используется HTTP транспорт.
  12. Обработка исключений. Обертывание всех исключений в FaultException, используя IErrorHandler.
  13. Закрытие канала посредника.
  14. Использование общей сборки с контрактами.
  15. Как убрать Web Reference. Использование общей сборки с контрактами.

Введение


В данной статье хочу описать настройки, которые разработчику следует знать  перед началом создания сервиса и клиента к нему. Я исхожу из того, что читатель уже имеет представление о WCF, пользовался какими то наработками, но не углублялся в настройки Binding и сервиса.
  • Если вы являетесь новичком и хотели бы увидеть примеры служб и работу с ними, перед прочтением советую скачать и посмотреть примеры Windows Communication Foundation (WCF) and Windows Workflow Foundation (WF) Samples for .NET Framework 4/ созданные Microsoft.
  • Из книг советую читать "Создание служб WCF (Джувел Леве)", на основе которой, а так же собственном опыте будет писаться данная статья.
  • Для демонстрации можно использовать мои примеры из проекта, ссылка на который в начале статьи. 
Я не буду расписывать все процессы создания с нуля. Тут будет представлены только наиболее интересные, на мой взгляд, понятия и настройки, о которых узнал после прочтения книги, просмотра описаний на msdn и написании примеров для демонстрации.
По каждой теме я старался не просто написать определение, а сделать реальный пример с демонстрацией, что бы все стало ясно.

Основные понятия и настройки.


Что такое endpoint?


Конечной точкой называется совокупность трех элементов:
  1. Адрес сервиса. Подразумевается, что это URL, по которому располагается сам сервис.
  2. Контракт сервиса.
  3. Binding - содержит настройки работы с сервисом, протокол взаимодействия, Timeouts операций и т.д.


Способы общения клиента с сервисом.


В WCF существует три способа общения, которые обязательно надо знать:
  1. Request-reply (запрос-ответ) - это когда с клиента делает вызов метода контракта Method(), текущий поток останавливается и ждет, пока сервер обработает ваш вызов и вернет ответ и управление. При этом все происходит в синхронном режиме, вы не сможете продолжить выполнение пока не получите ответа. Если происходит исключение на стороне сервера, то оно прилетит на клиента.
  2. OneWay (или datagram) - данный способ отличается тем, что вызываемая метод контракта сторона (это может быть клиент, или сервер при дуплексной связи), не останавливается при вызове, а продолжает выполнение. Как если бы вызов практически сразу вернул результат. Иными словами мы в любом случае ожидаем доставки SOAP сообщения на противоположную сторону, но не ожидаем получения результата в ответ.
    Возможны ситуации, когда и при таком взаимодействии тоже произойдет блокировка потока на вызываемой стороне:
    • Когда при вызове, на сервере мы упираемся в какой то нагрузочный maxConcurrent* придел сервиса и наше сообщение с вызовом помещается в очередь на обслуживание. В этом случае мы будем ожидать пока вызов не встанет в обработку либо отвалимся по Timeout.
  3. Duplex - это взаимодействие в обе стороны, когда клиент может вызывать методы сервера, а сервер, используя callback контракт, может вызывать методы клиента. Что бы это понять представьте чат, вы можете напечатать сообщение и отослать его на сервер, но что делать, если сообщение напишут вам? В этом случае сервер должен инициировать передачу на ваш компьютер. Как раз таки для этого он будет вызывать callback контракта, который задается отдельно, что бы уведомить вас.


Как реализуются данные схемы общения.

  1. Request-reply. Данное поведение является "по умолчанию".

    [ServiceContract]
    internal interface IMyContract 
    {
        [OperationContract]
        void Method();
    }
    
  2. OneWay. Для того, что бы метод контракта или callback контракта стал односторонним, достаточно указать IsOneWay = true.
    [ServiceContract]
    internal interface IMyContract
    {
        [OperationContract(IsOneWay = true)]
        void Method();
    }
    
  3. Duplex. Для реализации используется свойство атрибута под названием CallbackContract.
    
    
    [ServiceContract(CallbackContract = typeof(ICallback))]
    internal interface IMyContract
    {
        [OperationContract]
        void Method1();
     
        [OperationContract(IsOneWay = true)]
        void MethodOneWay1();
    }
     
    [ServiceContract]
    internal interface ICallback
    {
        [OperationContract]
        void Method2();
     
        [OperationContract(IsOneWay = true)]
        void MethodOneWay2();
    }


Канал связи клиента и сервера.


Под каналом стоит понимать связь, держащуюся между клиентом и сервером после соединения. Почти всем Binding достаточно одного соединения, исключение составляет WSDualHttpBinding в Duplex (при наличии callback контракта) режиме. Для обслуживания callback ему требуется дополнительно подключиться к клиенту на 80 порт (значение по умолчанию, может быть изменено через задание ClientBaseAddress адреса). В случае, если между клиентом и сервером настроен NAT, обратное соединение не удастся и придется настраивать на использование NetTcpBinding, у которой нет данной проблемы.

Надежные каналы связи.


Клиент и сервер обмениваются SOAP сообщениями. SOAP представляет из себя XML сообщения, доставку которых осуществляет TCP транспорт. Он гарантируется, доставку на уровне сетевых пакетов, но не гарантирует правильную передачу SOAP сообщений в ситуациях, когда есть проблемы с сетью, отключается сетевое подключение и прочее. Какие гарантии дают надежные каналы? Базовые определения написаны в статье WS-ReliableMessaging:
  • Каждое сообщение будет доставлено как минимум один раз. Если сообщение не может быть доставлено тогда обе стороны получат исключение.
  • Каждое сообщение будет доставлено не больше чем один раз.
  • Гарантировано будет доставлено ровно одно сообщение.
  • Сообщения будут доставлены получателю в порядке отправки. (Если включена настройка Ordered=true)
Честно сказать, после услышанного первые мой вопросы были:
- А как вообще WCF работает, если такие страшные вещи случаются? 
- И надо ли всегда использовать надежные каналы?

Всё не так печально как кажется. Если ваш сервис и клиенты работает в относительно надежной среде (допустим локальная сеть), тогда не стоит всем точкам включать этот режим. Очевидно, что он будет ударять по производительности, так как гарантированно придется производить дополнительные опросы о том, что пришло и не пришло и если используется Order, тогда может образовываться очередь на обработку. Поэтому советую использовать данный режим в ситуациях:
  • Когда клиент и сервер связанны неустойчивым каналом связи. К примеру: разделены интернетом, либо каким то промежуточным звеном (Proxy сервером или мостом).
  • Если цена ошибки очень высока. Допустим через точку доступа осуществляются какие то денежные операции.
  • Использование HTTP сессий (Работает только с WS* привязками). О сессиях будет сказано позже, но в силу природы HTTP транспорта, между сервером и клиентом нет постоянной TCP связи для того, что бы идентифицировать клиента. Поэтому в каждое сообщение будет добавляться особый идентификатор в HTTP header сообщения, что даст возможность использовать сессии с HTTP привязками.
Не все привязки поддерживают надежные сессии. Ниже я приведу только те, которые ее поддерживают, но не используют или используют по умолчанию.
  1. NetTcpBinding, WSHttpBinding, WSFederationHttpBinding - поддерживает, но опция не включена.
  2. WSDualHttpBinding - поддерживает и включена по умолчанию.
  3. NetNamedPipeBinding - не поддерживает, но опция включена всегда. Дело в том, что при использовании именованных каналов, по сетевой модели ISO мы не опускаемся даже до транспортного уровня, поэтому сообщения пересылаются всегда синхронно и упорядоченно.
Как же настраиваются надежные каналы?
Для включения следует использовать <reliableSession  /> внутри привязки со значением enabled="True". Для упорядоченной передачи есть атрибут ordered="true". Пример включение обоих параметров:
<netTcpBinding>
  <binding name="nettcp"
    <reliableSession enabled="True" ordered="true" /> 
  </binding>
</netTcpBinding>
Внимание! Ordered=True не может работать без включенной ReliableSession enabled=True.


Управление состоянием службы.


Когда делается вызов метода WCF сервер создает или использует экземпляр службы, обрабатывающий вызов клиента. Иными словами служба, это один экземпляр класса реализующего контракт. Служба ничего не знает о том, кто ее вызывает, и является stateless. Поэтому, если требуется как то идентифицировать пользователя или вызов, необходимо передавать какой то clientId в параметрах методов требуемых идентификации.

[ServiceContract]
public interface IMyService
{
    [OperationContract]
    void Hello(Guid clientId);
}

Выдача, хранение и контроль clientId лежит на разработчике сервиса.

Управление созданием экземпляров службы.


При написании сервиса необходимо проанализировать, каким образом должны создавать экземпляры служб внутри ServiceHost. Выбор будет очень сильно зависит от архитектуры работы и реакции на вызовы клиента. Режим задается через значение InstanceContextMode:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyClass : IMyService
{    
}

Существует три режима работы:
public enum InstanceContextMode
{
    PerSession,
    PerCall,
    Single,
}
  • Single (Singleton режим) - при первом обращении будет создан экземпляр службы, который будет использоваться всеми клиентами на протяжении жизни ServiceHost.
  • PerCall - каждый вызов метода будем приводить к созданию нового экземпляра службы, и это не зависит от того, используете вы перед каждым вызовом новый посредник или старый.
  • PerSession - (Режим по умолчанию) говорит о том, что будет создан один экземпляр для всего сеанса пользователя. Иными словами, если используется сессия, тогда пока экземпляр посредника не закрыт и им пользуются - экземпляр службы будет продолжать жить на сервере.
Сеанс пользователя - это время с момент первого вызова WCF метода созданного клиента контракта, и до момента его закрытия. Если в рамках клиентского приложения создать 5 клиентов (посредников), то на сервере будет 5 отдельных сеансов. Сеанс ограничен временем TCP соединением  клиента с серверов, поэтому поддерживается не всеми Bindings. NetTcpBinding и NetNamedPipeBinding без проблем могу давать возможность службе использовать сеансы. При этом BasicHttpBinding привязки по определению, не имеют постоянного соединения, в силу природы HTTP. Исключение составляют WS* привязки с включенной надежной сессией <reliableSession  /> (в рекомендациях к которой об этом было указано).
Для настройки использования сеансов у контракта можно задать значение SessionMode:

[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IMyServiceAllowed : IMyService
{
}

Данный параметр имеет три значения:
public enum SessionMode
{
    Allowed,
    Required,
    NotAllowed,
}
  • Allowed (Значение по умолчанию) - говорит о том, что разрешается использовать транспортные сеансы, но служба может работать и без них.
  • Required - служба требует наличия сеанса, и если привязка не поддерживает работу с сеансами, тогда клиент получит ошибку
    "Contract requires Session, but Binding 'BasicHttpBinding' doesn't support it or isn't configured properly to support it.".
  • NotAllowed - для работы требуется отсутствие сеансов.
Как же получить значение идентификатора сеанса пользователя, если сеанс используется? Для этого в методе службы, когда клиент его вызывает, можно обратиться к значению поля SessionId:

OperationContext.Current.SessionId

Для TCP, без надежной сессии, формат значения ниже (где N индекс сессии):
uuid:0ca714e3-b82c-4623-82e2-85954bdbbba4;id=N

Для HTTP и TCP с использованием надежной сессии формат значения это уникальный uuid:
urn:uuid:60323e4a-631a-471b-8ba3-668cc8f37703


В ситуации, когда сессия не используется, свойство Current будет null.

Теперь, используя пример "Service sessions demonstration" (код SessionExample.cs), который доступен по ссылке в начале статьи, я продемонстрирую работу с клиентскими сессиями. Класс реализующий службу:

public class MyService : IMyServiceIDisposable
{
    private int _call;
 
    public void Hello()
    {
        SysConsole.WriteLine("SessionId [{0}] Call [{1}]"OperationContext.Current?.SessionId, ++_call);
    }
 
    public void Dispose()
    {
        SysConsole.WriteLine("Goodbye [{0}]"OperationContext.Current?.SessionId);
    }
}

Когда клиент будет вызывать метод Hello сервер в Output напишет SessionId, если привязка поддерживает его, в противном случае [] пусто. Для демонстрации InstanceContextMode на экземплярах службы используется вывод количества вызовом этого метода ранее.

Демонстрация работы InstanceContextMode.PerSession + SessionMode.Required + NetTcpBinding + Ненадежная сессия.

На примере видно, что 2 клиента параллельно вызывают метод Hello, и для каждого экземпляра службы. Call [N] инкриминируется отдельно. В тексте SessionId видно, что используется две сессии, ...;id=1 и ...;id=2. В приложении можно легко сконструировать и проверить любые комбинации из привязок и режимов работы.


Управление временем жизни экземпляра службы атрибутами контракта.


Бывает так, что какой то бизнес процесс требует определенной последовательности действий от клиента. Допустим мы не можем разрешить пользователю пользоваться сервисом, пока он успешно не залогинится, а вызов метода отлогинивания сигнал о завершении сеанса общения. Для этого у OperationContract существует два свойства IsInitiating и IsTerminating.
  • IsInitiating (Значение по умолчанию True) - признак того, что клиент может начать свое первое обращение к службе с этого метода.
  • IsTerminating (Значение по умолчанию False) - признак того, что экземпляр службы и сеанс пользователя на сервере завершаются, сразу после вызова метода. Если таких методов несколько, тогда вызов любого из них приведет к такому результату.
После завершения сеанса общения (при вызова метода с пометкой IsTerminating) класс посредник клиента для работы со службой приходит в негодность. А у экземпляра службы, если она реализует IDisposable вызывается метод Dispose. Пример использования:

[ServiceContract(SessionMode = SessionMode.Required)]
public interface IMyService
{
    [OperationContract]
    void Login();
 
    [OperationContract(IsInitiating = false)]
    void SayHello();
 
    [OperationContract(IsTerminating = true)]
    void Logout();
}

Для того, что бы была возможность задавать IsInitiating и IsTerminating у контракта обязательно должна быть поддержка работы через сессию, поэтому SessionMode должен быть SessionMode.Required.
Если вам требуется управлять моментом дезактивации экземпляра службы, тогда это можно настроить через свойство атрибут OperationBehavior под названием ReleaseInstanceMode.

Учтите, что все равно надо закрыть посредника через Close(), что бы освободить ресурсы.

Для демонстрации есть пример под названием "Required operations sequence calls" (код OperationsCallSequenceExample.cs), использующий контракт выше. Пример работы:

Первым делом я вызываю Login, далее SayHello и по завершению Logout, после чего какой бы метод не будет вызван мы будем получать ошибку:
This channel cannot send any more messages because IsTerminating operation 'Logout' has already been called.


Режимы параллельной работы с экземплярами службы.


После того, как определились с режим создания экземпляров служб параметром InstanceContextMode, необходимо подумать, как службы должны вести себя в режиме параллельной работы. Выбор режима необходим, потому что параллельная работа с экземпляром накладывает на нее необходимость быть потокобезопасной. То есть объекты, отвечающие за состояние службы, должны блокироваться когда один из потоков начинает с ними работать. Это делается для того, что бы не повредить состояние. Для настройки параллельной работы используется параметр ConcurrencyMode. Все его значение представлены ниже:
public enum ConcurrencyMode
{
  Single,
  Reentrant,
  Multiple,
}
  • ConcurrencyMode.Single (режим работы по умолчанию) означает, что с экземпляром службы может работать только один поток. Соответственно параллельные вызовы невозможны, и потребность в блокировках ресурсов, внутри службы, нет. При этом, из службы запрещено вызывать другие сервисы, так как это может привести к повторному обращению к службе и взаимной блокировке вызова.
  • ConcurrencyMode.Multiple говорит о том, что вызовы никак не синхронизируются и программисту требуется самому управлять блокировками ресурсов отвечающие за состояние. 
  • ConcurrencyMode.Reentrant (Re-entrant повторно входящий) представляет из себя режим Single, с возможностью повторного входа. Что дает возможность вызывать другие службы и использовать callback методы для работы с клиентом
Когда я хотел сделать пример, для демонстрации всех режимов я был удивлен, что режим Multiple работает не так, как я всегда думал. Поэтому я приведу пример не только того, как меняя режимы видно изменения в многопоточной работе службы, но и пример, в котором будет многопоточный клиент работающий с сервисом, но сервис не обрабатывает все запросы клиента одновременно.

Особенности демонстрации ниже
  • Proxy посредник, для работы со службой, создается в единственном экземпляре.
  • В несколько потоков происходит работа с этим единственным посредником.
  • Первый поток работает с методом Request-Reply. Второй использует метод OneWay, тем самым обработка его очереди запросов проходит сразу и в параллельном режиме.
  • Значение InstanceConectMode используется по умолчанию, а значит PerSessions
  • Будет использована NetTcpBinding привязка.


Первый пример


Для демонстрации режима ConcurrencyMode можно посмотреть "ConcurrencyModes demonstrations (Works only when one client proxy in use)" (код ConcurrencyModeExample.cs). Вы не увидите пример Reentrant, поскольку он такой же, как Single.

Пример ConcurrencyMode.Single:

Как видно в режиме Single интервал между вызовами ровно 5 секунд. А OneWay метод и Request-Reply проходят процедуру блокировки.

Пример ConcurrencyMode.Multiple:
В режиме параллельной видно, что никакой синхронизации не происходит.


Второй пример.


Теперь самое интересное, как думаете, что будет если:
  1. Используется только request-reply метод у единственного экземпляра посредника и несколько потоков.
  2. ConcurrencyModes.Multiple - значит разрешается многопоточная обработка запросов.
  3. InstanceContextMode.PerCall - значит каждый вызов метода клиентом создает новый экземпляр службы
  4. Буду использовать BasicHttpBinding, но такой же результат для NetTcpBinding с\без сессии.
Внимание! В ситуации, если в каждом потоке создается свой экземпляр посредника, это приводит к параллельной обработке запросов на стороне сервера. Если использовать данную схему, то у вас проблем, описанных ниже, не возникнет.
Казалось бы, для каждого вызова из потока будет создаваться новый экземпляр службы, при этом параллельная работа разрешена, а значит, по логике, во сколько потоков вызывать метод, столько и одновременно обрабатываемых запросов будет. Но это не так.

Для демонстрации стоит запустить пример под названием "ConcurrencyModes.Multiple + InstanceContextMode.PerCall + MultiThread_Client_Calls not working for Request-Replies" (код ConcurrencyModeNotWorkingExample.cs) после запуска результат будет полная синхронизация вызовов, хотя ожидалась параллельная работа:

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

Регулирование нагрузки.


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

<behaviors>
  <serviceBehaviors>
    <behavior name="">
      <serviceThrottling 
        maxConcurrentCalls="64" 
        maxConcurrentSessions="64" 
        maxConcurrentInstances="64" />
    </behavior>
  </serviceBehaviors>
</behaviors> 

У всех у них одинаковый префикс maxConcurrent, означающий максимальное количество параллельных операций или объектов.
Важно! После их прочтения должен возникнуть закономерный вопрос "Данная настройка относится к каждой службе или к всему серверу?". Так вот, данное ограничение применяется к каждому экземпляру ServiceHost, и уже вам надо глядеть, как ваше решение создаёт экземпляры, и как использует.
  • maxConcurrentCalls (значение по умолчанию 16*ProcessorCores) - означает максимальное количество параллельных вызовов. Что является одним вызовом? На стороне клиента, один вызов метода класса посредника и ожидание его результата. При этом, суммирование максимального количества идет по каждому ожидаемому вызову классов посредников на стороне каждого клиента. В ситуации, когда превышается пороговое значение, все последующие вызовы встают в очередь и могут, в случае загруженности сервера, отключиться с TimeoutException.
  • maxConcurrentSessions (Значение по умолчанию 100*ProcessorCores) - максимальное количество сессий, которые могут быть активны в любой момент времени. Как и при параллельных вызовах, новые запросы, превышающие это значение и требующие сессии, встают в очередь.
  • maxConcurrentInstances (значение по умолчанию сумма maxConcurrentSessions и maxConcurrentCalls) - максимальное количество InstanceContext объектов у сервиса. Иными словами, это максимальное количество экземпляров служб, которые могу существовать в любой промежуток времени. На данную настройку напрямую влияет значение InstanceContextMode характеризующий режим создания экземпляров служб.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] 
Важно! Значения по умолчанию актуальны на момент публикации статьи и относятся к последней версии .NET Framework. Было замечено, что в разных версиях цифры отличаются. Поэтому для нахождения значения под вашу версию, можно воспользоваться меню Other versions на сайте msdn.

При использовании NetTcpBinding и NetNamedPipeBinding можно указать максимальное число TCP подключений от клиента до конечной точки. Задается оно через параметр maxConnections (значение по умолчанию 10):

<bindings>
  <netTcpBinding>
    <binding name="ConnectionsBinding"  maxConnections="30" />
  </netTcpBinding>
</bindings>

Что это за параметр, и нужно ли его менять?
Данное название, мне кажется, не является удачным, так как приводит к мысли, что это максимальное количество исходящих от клиента соединений к серверу, но это не так. Данное значение, говорит о том, какое количество TCP соединений будет размещено в пуле соединений для дальнейшего повторного использования. То есть никаких ограничений на создание нет, лишь дальнейшее использования точки доступа избавит от повторной инициализации TCP соединения, а возьмет (в случае наличия) свободное соединение из пула очереди. Настройка полезна тем, чье общение с сервером это череда создания каналов, вызов методов и сразу его закрытия.

Что бы наглядно увидеть результат можно посмотреть пример "TCP connections limit in NetTcpBinding [maxConnections]." (код ConnectionsExample.cs) в тестовом приложении, ссылка на которое вверху страницы.
Я указал 4 подключения и запустил создание 15 клиентов, каждый из которых создал посредника, вызвал метод и закрыл подключение. В итоге спустя 5-10 секунд в списке подключений у клиента осталось ровно 4 подключения к серверу. Что бы увидеть окно с подключениями я использовал программу Process Explorer.
Важно! В случае, если на момент закрытия не останется больше каналов между клиентом и сервером, то последний закрытый канал не оставит 4 подключения (из примера выше) а закроет их. Поэтому с целью демонстрации в примерах есть holderClient, цель которому не закрыть все соединения а показать результат


Обработка исключений.


В ходе общения клиента с сервером может возникать ситуация, когда обрабатывающая вызов сторона прерывает обработку и выбрасывает исключение. Это приводит к переводу канала переходит в Fault состояние и все последующие вызовы методов посредника будут заканчиваться с CommunicationObjectFaultedException. Клиенту ничего не остается сделать, как зарыть посредника и создать нового. Такая же ситуация, если мы работаем с методом обратного вызова (Callback), и клиент вернул исключение, в этом случае невозможно далее использовать методы обратного вызова, но клиент всё равно без проблем может продолжать вызывать методы сервера у экземпляра посредника.
Что бы исключения не приводили к разрыву связи, необходимо использовать FaultException. Проблема этого исключения в том, что на обрабатываемой вызов стороне сложно типизировать исключение. Для типизации у FaultException есть Generic тип FaultException<>, которым можно типизировать исключение. К примеру в случае если заранее указать ArgumentNullException у метода, то с сервера будет прилетать FaultException<ArgumentNullException>.

Краткое описание каждого из исключений:
  • FaultException - можно выбрасывать службой без каких либо ограничений
    throw new FaultException("New fault exception");
    
  • FaultException<> - что бы со службы вернуть типизированное исключение требуется предварительно у метода контракта указать тип исключения, через атрибут FaultContract:
    [ServiceContract]
    interface IMyService {
        [OperationContract]
        [FaultContract(typeof(ArgumentNullException))]
        void MethodFaultContract();
    И далее в методе MethodFaultContract() можно выбрасывать исключения вот таким образом:
    throw new FaultException<ArgumentNullException>(new ArgumentNullException(), "Something null");
    
Для демонстрации можно посмотреть пример под названием "FaultException. Simple methods calls." (код  FaultExceptionExample.cs), где вызывается код выше. Результат работы:

Важно! Интересная особенность работы WCF представлена на примере метода MethodWrongFaultContract. В контракте должно выбрасываться только InvalidCastException исключение, но метод пытается выбросить ArgumentNullException в этом случае на клиент приходит обычное FaultException.


Обработка исключений. Продолжение работы с посредником если используется HTTP транспорт.


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

Для демонстрации можно посмотреть  пример под названием "netTcpBinding/wsHttpBinding/basicHttpBinding channel exceptions with ReliableSession [True/False]" (код ReliableSessionExceptionExample.cs). Сервис выглядит таким образом:
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class Service : IService
{
    public void MethodThrowingException()
    {
        throw new AccessViolationException("AccessViolationException from server IsOneWay = true");
    }
 
    public void OneWayMethodThrowingException()
    {
        throw new AddressAlreadyInUseException("AddressAlreadyInUseException from server");
    }
 
    public void Method()
    {
        SysConsole.WriteLine($"Hello [{OperationContext.Current?.SessionId}]");
    }
}

Нам понадобится HTTP транспортные привязки без надежного канала связи (так как в этом случае сессия создана не будет). Поэтому из netTcpBinding, wsDualHttpBinding и BasicHttpBinding подойдет лишь две комбинации:
  • BasicHttpBinding - так как не может иметь сессию вприципе.
  • WSHttpBinding (либо WSDualHttpBinding) - используя без надежного канала (reliableSession), который по умолчанию включен.
Результаты запуска представлены ниже. На них видно, что посредник вызывается без проблем 3 раза для wsHttpBinding без надежной сессии:

И пример netTcpBinding для этого же сервиса, где мы видим отсутствие возможности вызова методов после первого же исключения:


Обработка исключений. Обертывание всех исключений в FaultException, используя IErrorHandler.


Если в вашем приложении нет логики на обработку типизированных исключений, а любой разрыв соединения дорого обходится. Тогда можно реализовать универсальный способ передачи на клиента любого исключения в виде FaultException. Для этого необходимо добавить у ChannelDispatcher в коллекцию обработчиков ErrorHandlers добавить HOOK на обработку исключений перед отправкой клиенту.  Внутри обработчика, который должен реализовывать IErrorHandler, мы у исключения находим самое нижнее InnerException, создаем на основе его текста FaultException и пересылаем на клиента.

Демонстрация работы представлена в примере под названием "IErrorHandler exceptions wrapper." (код ErrorHandlerExample.cs).

Для того, что бы нам добавить обработчик, необходимо реализовать IEndpointBehavior и в методе ApplyDispatchBehavior можно добавить наш HOOK. Результат работы для контракта, без описания исключения и выбрасывающий NullReferenceException представлен ниже:

[ServiceContract]
interface IErrorService
{
    [OperationContract]
    void MethodThrowingException();
    
    [OperationContract]
    string CheckClient();
}



Как видно, после исключения вызов CheckClient отработал нормально.

Закрытие канала посредника.


Для завершения требуется вызвать метод IChannel.Close(). Если вы используете класс посредник созданный студией, то у него достаточно вызвать напрямую метод Close() либо Dispose(). Если же вы используете посредника созданного через ChannelFactory<TContract>.CreateChannel(), тогда вам его придется привести к ICommunicationObject, после чего вызывать метод закрытия. Даже при закрытии может произойти исключение, поэтому для правильной обработки стоит использовать шаблонный try {} catch {} с сайта Microsoft. Ссылка на шаблон: Avoiding Problems with the Using Statement.
try  
{  
    ...  
    client.Close();  
}  
catch (CommunicationException e)  
{  
    ...  
    client.Abort();  
}  
catch (TimeoutException e)  
{  
    ...  
    client.Abort();  
}  
catch (Exception e)  
{  
    ...  
    client.Abort();  
    throw;  
}  
В ситуации, если клиент не закроет сеанс, тогда экземпляр службы на сервере будет жить по умолчанию 10 минут. Это значение можно поменять, задав значение inactivityTimeout:
<reliableSession enabled="True" inactivityTimeout="00:02:00" /> 
В данном случае  значение Timeout станет 2 минуты.

Как убрать Web Reference. Использование общей сборки с контрактами.


Если клиент и сервер являются частью вашей системы, тогда можно отказаться от использования "Service Reference" или "Web Reference" у Visual Studio в вашем решении. Обычно, когда в контракте происходят изменения, программисту надо у клиента в свойствам выбрать пункт обновления ссылки:



Если забыть это сделать, тогда нельзя гарантировать корректную работу со службой. Что бы этого избежать можно создать библиотеку, которая будет как на клиенте так и на сервере. В неё сложить все используемые контракты, после чего посредников на клиенте создавать через ChannelFactory, а на сервере использовать контракты сборки. Тем самым меняя контракт в одном месте гарантировано он будет актуальным как на сервере так и на клиенте.

11 комментариев:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. Спасибо то что нужно краткий справочник. А то прочитал книгу как будто видел фигу так в итоге и не понял по InstanceContextMode, SessionMode и ConcurrencyMode

    ОтветитьУдалить
  3. А в чем разница между ConcurrencyMode.Single и Reentrant
    У меня сейчас стоит по умолчанию ConcurrencyMode.Single и я пользуюсь callback(методами на стороне клиента), не ужели разница только в том что сервер может сам к себе отключиться и все а смысл? не проще создать общий метод воид и без подключения обойтись?

    ОтветитьУдалить
  4. Добрый день, рад что нашли что то полезное.

    Дело в том, что архитектурный подходы у всех разные и по невнимательности или не знанию работы системы при вызове метода сервера сервер, сервер в ответ вызовет callback клиента а клиент снова сервер и т.д. Это приведёт к зацикливанию и зависанию клиент и возможно сервера если исчерпается кол-во maxConcurrentCalls или экземпляров службы если если PerCall. Поэтому стоит продумать возможность такого поведения службы заранее. А если случится повторный вызов и программист поймает исключение, то это может значительно уменьшить головную боль на продашине. А контролировать такое поведение в коде - свихнёшься

    Я лично сталкивался с зацикливанием.

    ОтветитьУдалить
  5. А не подскажите почему на WCF не желательно чтобы было много методов больше 20
    Джувел Лёве "Создание служб WCF" 579 ст. "Контракт службы не должен содержать более 20 компонентов. Вероятно, на практике верхний предел 12"

    Как такового четкого ответа почему я не нашел но у меня при [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    уже 45 набралось и разделить возможности нету так как весь замысел что я 1 раз делаю авторизацию и канал живет вечно а я по мере необходимости оповещаю всех кто был авторизован.

    ОтветитьУдалить
    Ответы
    1. Про какие то ограничения на кол-во методов я не знаю. С авторизацией лучший способ (он же в книге описан) выдавать какойто GUID после авторизации пользователя, запоминать его на сервере и при обращении к методу контракта передавать этот GUID что бы сервер понимал кто перед ним.

      Пример можете поглядеть у меня в игре "Шахматы" https://www.devowl.net/2018/09/network-chess-game-csharp-source-code.html

      Так же, что бы в 40 методах не добавлять параметр достаточно добавить 1 метод в контракте Authorizate(GUID userId) c пометкой

      [OperationContract(IsInitiating = true)]

      Который проверит userId и в случае наличия этого пользователя даст работать дальше со службой (опять же в шахматах есть этот пример).

      Еще один минус, если у вас 1 контракт то вы не сможете точечно настроить какую то группу контрактов, вам придётся писать Binding для всех 40 методов одного вида.

      Тот же Duplex стоит вынести в отдельный контракт точно.

      Готов поспорить что 40 методов можно как то логически разделить если подумать часик :)

      Удалить
  6. А вы случайно не работали с SecurityMode.Transport?

    serviceHost.AddServiceEndpoint(typeof(IServices), new NetTcpBinding(SecurityMode.Transport), "");

    Мне понравилось когда работая на одном компе клиент и служба с настройками по умолчанию при разрыве соединения клиент сразу же узнает о разрыве соединения
    Но вот беда запустил с разных компов и полезли ошибки что то типо SSL.
    Возможно ли на SecurityMode.Transport использовать какую нить свою проверку или по отключать (просто при TcpClientCredentialType.None или SslProtocols.None - перестает работать все)

    Или может есть какой то другой вариант при отправки void на Duplex первое сообщение при разорванном канале пропадает бесследно ошибка вылазиет только после 2 отправки любова метода. (вот пример если я не понятно не по русски вывозился http://www.cyberforum.ru/web-services-wcf/thread2432075.html#post13474296 я думал там что проблему решил выставив на SecurityMode.Transport но не тут то было теперь для релиза нужен сертификат который приобрести нету возможности)

    ОтветитьУдалить
  7. SecurityMode.Transport не использовал, для корректного разрыва требуется клиент закрывать service.Close(), если на канальном уровне произойдёт отключение, тогда сервер узнает об отключении когда произойдёт таймаут привязки. Соответвенно если выставить огромное время то и ожидать будете соотвевенно.

    Если ваш контракт с сессией (постоянным соединением), тогда можно использовать пример выше

    public class MyService : IMyService, IDisposable

    Где в Dispose() при разрыве я получал уведомление о разрыве соединения

    ОтветитьУдалить
    Ответы
    1. Да подключение постоянное ReceiveTimeout = new TimeSpan(24, 0, 0)
      Но пока как что на сервере отвалившиеся клиенты это не головная боль.
      var tr = (IClientChannel)CurrentCallback;
      tr.Closed += ClientDisconnected;
      tr.Faulted += ClientDisconnected;
      void ClientDisconnected(object sender, EventArgs e)
      {
      try
      {
      var tr = clients.Where(x => x.ОбратнаяСвязь == sender).ToList();
      clients.Remove(clients.FirstOrDefault(x => x.ОбратнаяСвязь == sender));
      }
      catch (Exception) { }
      }

      И при рассылке в новых потоках проблемный клиент через 5 минут выкидывает исключение и удаляется конечно за 5 минут на этот порванный канал можно отправить кучу всего но при маленькой нагрузке мне не критично, а вот с клиентом к примеру отправляя к примеру заявку безответную IsOneWay = true только после отправки 2 заявки через минуту я получаю ответ что канал разорван и в итоге теряю 2 заявки и сново их набирать.

      Менять IsOneWay на false и на все вопросы возвращать просто true это значит менять всю логику и переходить на многапоточность чего бы на сегодняшний день не хотелось делать
      bool Метод(){

      ThreadPool.QueueUserWorkItem(ДругойМетод);

      return true;
      }

      Удалить
  8. Либо вести учет всех отправленных методов и на клиенте и на сервере после сервер отправляет какие получил чтобы клиент не посылал повторно. Но это вообще из разряда бред.

    Как и 3 вариант сделать метод который сразу возвращает true
    после на клиенте перед основным запросом спрашиваю этот метод получаю true отправляю основной метод потом опять отправляю метод. Но опять же если разрыв произошел после отправки основного метода то клиент посчитает что основной метод не дошел

    Че же эти мелкомягкие не додумались сделать по мимо сертификата и виндовс аутерификации на SecurityMode.Transport еще какой нить свой метод

    ОтветитьУдалить