Показаны сообщения с ярлыком WCF. Показать все сообщения
Показаны сообщения с ярлыком WCF. Показать все сообщения

четверг, 14 ноября 2013 г.

WCF и архитектура приложений

В этой заметке я отталкиваюсь от определения архитектуры, которое предложил Мартин Фаулер в своей статье “Who Needs an Architect?”, перевод которой я публиковал в прошлый раз.

В чем главное отличие WCF от своих предшественников, таких как .NET Remoting, веб-сервисы, MSMQ или более старых технологий, таких как DCOM?

Расширяемость? Так .NET Remoting тоже содержит 100 500 слоев и расширяем донельзя. Возможность взаимодействия с приложениями на других платформах? Так это же умели делать и веб-сервисы. Ориентированность на обмен сообщений? Так именно эта идея заложена в основе MSMQ.

среда, 26 октября 2011 г.

Проблемы передачи списка перечислений или Почему абстракции «текут»

Все нетривиальные абстракции дырявы
Джоэл Спольски – Закон дырявых абстракций

А иногда дырявятся и довольно простые абстракции
Автор этой статьи

Большинство современных разработчиков знакомы с «законом дырявых абстракций» по знаменитой заметке Джоэла Спольски с одноименным названием. Заключается этот закон в том, что как бы ни был хорош протокол взаимодействия, фреймворк или набор классов, моделирующих предметную область, рано или поздно нам приходится спускаться на уровень ниже и разбираться с тем, как же эта абстракция устроена. Внутреннее устройство абстракции должно быть проблемой самой абстракции, но возможно это только в наиболее общих случаев и лишь до тех пор, пока все идет хорошо (*).

Когда-то давно, в «небольшой» мелкомягкой компании решили, а почему бы нам не «абстрагироваться» от местоположения объекта и сделать сам факт того, является ли объект локальным или удаленным, лишь «деталью реализации». Так появились технологии DCOM и ее наследник .NET Remoting, которые скрывали от разработчика, является ли объект удаленным или нет. При этом появились все эти «прозрачные прокси», которые позволяли работать с удаленным объектом, даже не зная об этом. Однако, со временем выяснилось, что эта информация архиважна для разработчика, поскольку удаленный объект может генерировать совершенно другой перечень исключений, да и стоимость работы с ним несравнимо выше, чем взаимодействие с локальным объектом.

четверг, 24 февраля 2011 г.

Что такое WCF?

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

Общие сведения

Если в двух словах, то WCF (a.k.a. Windows Communication Foundation) — это очередной фреймворк для построения распределенных приложений и межпроцессного взаимодействия, который является логическим развитием предыдущих подобных технологий компании Майкрософт, в частности Веб-сервисов, .Net Remoting и DCOM. И если предшественники были заточены на выполнение какого-то конкретного круга задач, то WCF - это скорее мультипарадигменная технология, вобравшая в себе все лучшее от своих предшественников, добавив при этом, конечно же, кое-каких собственных проблем.

Существенным отличием WCF от .Net Remoting является то, что WCF — это, прежде всего, технология для построения сервис-ориентированной архитектуры приложений (SOA — Service-Oriented Architecture), что позволяет абстрагироваться от конкретной технологи, на которой этот сервис реализован и пользоваться им из других приложений, написанных на любой другой платформе, языке, технологии; главное, чтобы реализация клиента отвечала определенным правилам. Кроме того, логика самого сервиса и его реализация полностью отделена от коммуникационной составляющей, и мы можем декларативно изменять способ взаимодействия с сервисом путем изменения конфигурационного файла. Мы можем изменить протокол взаимодействия, адрес, настроить максимальное количество подключений, ограничить размер пакетов и тайм-аут подключения к сервису, тайм-аут выполнения операции, надежность доставки и многое другое.

понедельник, 24 января 2011 г.

[WCF] Декларативное использование NetDataContractSerializer-а

Я уже неоднократно писал о проблемах известных типов (Known Types) в WCF и о нескольких способах ее решения. При этом я также упоминал, что одним из наиболее радикальных способов решения этой проблемы является использование сериализатора NetDataContactSerializer, вместо DataContractSerializer, используемого по умолчанию. Основное отличие первого класса от второго заключается в том, что NetDataContractSerializer сохраняет информацию о CLR-типе в сериализированный поток байтов, передаваемый между сервисом и его клиентом. Такое поведение нарушает ключевой принцип сервис-ориентированной архитектуры (SOA, Service-Oriented Architecture), который гласит о том, что сервисы и их клиенты не должны ничего знать о тех платформах, языках или технологиях, на которых они работают. Однако иногда мы можем себе позволить пойти на такие жертвы, когда четко знаем, что наша система предназначена только для платформы .Net, а другие языки и технологии использоваться не будут.

Наиболее популярным в сети способом использования NetDatacontractSerializer-а заключается в создании custom-атрибута, которым нужно пометить все методы сервиса или сам сервис целиком (этот способ я описывал в одной из предыдущих заметок и именно его я использовал на практике), однако появились сведения, что в некоторых случаях это может привести к проблемам, поскольку в этом случае производится изменение поведения (operation behavior) уже после вызова метода OpenHost, и в некоторых случаях может привести к непредсказуемому поведению. Другим, не менее важным недостатком того подхода является то, что вам нужно захардкодить ваше решение прямо в коде (путем добавления этих атрибутам к классам и/или методам) и нельзя перейти от одного решения к другому без перекомпиляции приложения. Кроме того, этот вариант не работает совместно с получением информации о сервисе посредством mex (Metadata Exchange Endpoints), поскольку в этом случае будут сгенерированы классы и интерфейсы без этого атрибута и попытка их использования ни к чему хорошему не приведет. В данном случае клиент будет использовать сериализатор DataContractSerializer, а сервис NetDataContractSerializer, и хотя данные сериализованные первым сериализатором могут быть десериализированы вторым, в обратном направлении это работает не всегда.

четверг, 3 сентября 2009 г.

Применение NetDataContractSerializer в WCF

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

Частично решить эту проблему можно путем задания известных типов (Known Types), причем сделать это можно самым различным способом (я описывал все эти способы в цикле статей «Известные типы в WCF», часть 1, часть 2, часть 3 и часть 4). Но этот вариант не всегда является удобным решением. Приложение может перейти на WCF с .Net Remoting, в котором используется механизм форматеров для сериализации/десериализации и существует возможность передавать между клиентом и сервером любые сериализируемые сущности. Кроме того, даже в новом приложении, использующем WCF как на стороне сервиса, так и на стороне клиента, могут применяться сложные объекты в качестве аргументов и возвращаемых значений операций контракта. И хотя это не отвечает принципам сервис-ориентированного программирования, разработчики могут сознательно пойти на этот шаг, зная, что их приложение не предусматривает разработку клиентских частей с использованием других технологий.

Невозможность передачи полиморфных объектов связано с тем, что по умолчанию инфраструктура WCF использует DataContractSerializer для сериализации/десериализации объектов. Основной особенностью этого сериализатора является то, что информация о типе не сохраняется при сериализации, а берется из контракта операции. Это вполне естественное поведение, ведь WCF – это реализация промышленного стандарта по созданию сервис-ориентированных приложений, а такие приложения являются нейтральными к технологии, на которых создается сервис или его клиент, а тип параметра относится к специфике CLR, а значит является деталью реализации.

Кроме DataContractSerializer в WCF предусмотрена возможность применения других типов сериализаторов, таких как старый добрый XmlSerializer, NetDataContractSerializer и DataContractJsonSerializer. XmlSerializer для своей работы требует, чтобы сериализируемый класс был публичным, имел конструктор по умолчанию и сериализирует вначале все открытые поля в порядке объявления, а затем свойства (read/write properties) также в порядке объявления. Для применения XmlSerializer вместо DataContractSerializer необходимо пометить отдельный метод сервиса, либо класс сервиса целиком атрибутом XmlSerializerFormatAttribute.

NetDataContractSerializer работает аналогично DataContractSerializer, и имеет прямую и обратную совместимость с DataContractSerializer, что позволяет сериализовать объект одним сериализатором, а десериализовать другим.

NetDataContractSerializer добавляет полную информацию о сериализируемом типе в поток данных, что позволяет загрузить необходимую сборку при десериализации и требует совместного использования типом между сервисом и клиентом. Совместное использование типов (sharing types) нарушает принципы сервис-ориентированного программирования, которые основаны на разделении контрактов (sharing contracts), поэтому разработчики WCF не предусмотрели простого способа использования NetDataContractSerializer в приложениях.

Для возможности использования NetDataContractSerializer c определенной операцией, контрактом или службой целиком, необходимо реализовать один из следующих интерфейсов IOperationBehavior, IContractBehavior и IServiceBehavior соответственно. Каждый из этих интерфейсов имеет одинаковую функциональность и сводится к замене поведения с типом DataContractSerializerOperationBehavior на объект собственного класса, возвращающий необходимый сериализатор. Наиболее простым способ применения является создание класса наследника от System.Attribute и применение созданного атрибута к операции, контракту или сервису целиком.

 
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class |
    AttributeTargets.Interface)]
public class NetDataContractFormatAttribute :
    Attribute, IOperationBehavior,
    IServiceBehavior, IContractBehavior
{
    void IOperationBehavior.AddBindingParameters(
        OperationDescription description, BindingParameterCollection parameters)
    { }
 
    void IOperationBehavior.ApplyClientBehavior(
        OperationDescription description,
        ClientOperation proxy)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.ApplyDispatchBehavior(
        OperationDescription description,
        DispatchOperation dispatch)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.Validate(
        OperationDescription description)
    { }
 
   void IServiceBehavior.AddBindingParameters(
        ServiceDescription serviceDescription,
        ServiceHostBase serviceHostBase,
        Collection<ServiceEndpoint> endpoints,
        BindingParameterCollection bindingParameters)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }
 
    void IServiceBehavior.ApplyDispatchBehavior(
        ServiceDescription serviceDescription,
        ServiceHostBase serviceHostBase)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }
 
    void IServiceBehavior.Validate(
        ServiceDescription serviceDescription,
        ServiceHostBase serviceHostBase)
    { }
 
    void IContractBehavior.AddBindingParameters(
        ContractDescription contractDescription,
        ServiceEndpoint endpoint,
        BindingParameterCollection bindingParameters)
    { }
 
    void IContractBehavior.ApplyClientBehavior(
        ContractDescription contractDescription,
        ServiceEndpoint endpoint,
        ClientRuntime clientRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }
 
    void IContractBehavior.ApplyDispatchBehavior(
        ContractDescription contractDescription,
        ServiceEndpoint endpoint,
        DispatchRuntime dispatchRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }
 
    void IContractBehavior.Validate(
        ContractDescription contractDescription,
        ServiceEndpoint endpoint)
    { }
 
 
    private static void ReplaceDataContractSerializerOperationBehavior(
        ServiceDescription description)
    {
        foreach (var endpoint in description.Endpoints)
        {
            ReplaceDataContractSerializerOperationBehavior(endpoint);
        }
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(
        ContractDescription description)
    {
        foreach (var operation in description.Operations)
        {
            ReplaceDataContractSerializerOperationBehavior(operation);
        }
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(
        ServiceEndpoint endpoint)
    {
        // ignore mex
        if (endpoint.Contract.ContractType == typeof(IMetadataExchange))
        {
            return;
        }
        ReplaceDataContractSerializerOperationBehavior(endpoint.Contract);
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(
        OperationDescription description)
    {
        var behavior = description.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (behavior != null)
        {
            description.Behaviors.Remove(behavior);
            description.Behaviors.Add(new NetDataContractSerializerOperationBehavior(description));
        }
 
    }
 
    public class NetDataContractSerializerOperationBehavior
        : DataContractSerializerOperationBehavior
    {
        public NetDataContractSerializerOperationBehavior(
            OperationDescription description)
            : base(description) { }
 
        public override XmlObjectSerializer CreateSerializer(
            Type type, string name,
            string ns, IList<Type> knownTypes)
        {
            return new NetDataContractSerializer();
        }
 
        public override XmlObjectSerializer CreateSerializer(
            Type type, XmlDictionaryString name,
            XmlDictionaryString ns, IList<Type> knownTypes)
        {
            return new NetDataContractSerializer();
        }
    }
}
 
Пример использования следующий:
 
//Применение к отдельной операции
[OperationContract]
[NetDataContractFormat]
Shape CreateShape(ShapeType shapeType, int id);
 
//Применение к контракту
[ServiceContract(CallbackContract = typeof(IServiceCallback), 
    SessionMode = SessionMode.Required)]
[NetDataContractFormat]
public interface IService { ... }
 
//Применение к сервису
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, 
    ConcurrencyMode = ConcurrencyMode.Multiple, 
    UseSynchronizationContext = false)]
[NetDataContractFormat]
public class Service : IService { ... }
 

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

среда, 2 сентября 2009 г.

Известные типы в WCF. Часть 4

Задание обобщенных известных типов

Обобщенные типы (generic types) играют важную роль при программировании на платформе .Net, но их применение весьма ограничено, когда речь заходит о разработке контрактов сервисов. Это связано с тем, что обобщенные типы относятся к специфике платформы .Net, а одной из основных парадигм сервис-ориентированного программирования является публикация контракта и сокрытие деталей реализации.

Хотя применение обобщенных методов в качестве контракта сервиса невозможно, применение «закрытых» (closed) обобщенных типов вполне допустимо, если аргумент типа является допустимым в качестве контракта данных.

ПРИМЕЧАНИЕ
Закрытым обобщенным типом (closed generic type) называется обобщенный тип, которому всем аргументам-типам переданы действительные типы данных. Таким образом, List является открытым типом, а List - закрытым.

[DataContract]
public class Shape<T> { ... }
 
[DataContract]
public class Circle<T> : Shape<T> { ... }
 
[DataContract]
public class Square<T> : Shape<T> { ... }
 
[ServiceContract]
public interface IService
{
    [OperationContract]
    void ProcessShapeOfInt(Shape<int> shape); //Вариант 1
 
    [OperationContract]
    void ProcessShape<T>(Shape<T> shape); //Вариант 2
 
    ...
}

Применение операции ProcessShapOfInt вполне допустимо, в то время, как применение операции ProcessShape приведет к следующей ошибке:

ERROR: System.Runtime.Serialization.InvalidDataContractException: Невозможно экспортировать тип "Server.Shape`1[T]" как тип схемы, так как он является открытым базовым типом. Базовый тип можно экспортировать, только если все типы его базовых параметров являются реальными типами.

ПРИМЕЧАНИЕ
Возможность применения закрытых типов обусловлено тем, что при импортировании метаданных контракта все закрытые обобщенные параметры переименовываются согласно следующей схемы: <исходное имя>of<имена параметров типов><хеш>. Таким образом метаданные службы не содержат информации об обобщенных параметрах и не нарушают принципы сервис-ориентированного программирования.

Как и применение обобщенных типов в операциях контракта, добавление обобщенных типов в список известных типов имеет ряд особенностей.

Рассмотрим следующий код:

[DataContract]
[KnownType(typeof(Circle<>))]
[KnownType(typeof(Square<>))]
public class Shape<T> { ... }

Хотя приведенный код компилируется и запускается, при обновление метаданных с помощью Metadata Exchange Endpoint будет невозможно. При попытке обновления метаданных с помощью Microsoft Visual Studio вы получите ошибку: «Метаданные содержат неразрешимую ссылку: “net.tcp://localhost:6101/WCFTestService/mex”». Чтобы понять точнее, что же не так с метаданными, необходимо воспользоваться утилитой SvcUtil.exe. В этом случае ошибка будет более понятной:

Ошибка при получении известных типов для типа "Server.Square`1[T]". Этот тип не должен быть открытым или частичным базовым классом.

Для задания обобщенных известных типов с помощью атрибута KnownType необходимо воспользоваться версией конструктора, принимающей имя метода:

[DataContract]
[KnownType("GetKnownTypes")]
public class Shape<T>
{
    ...
    static IEnumerable<Type> GetKnownTypes()
    {
        return new [] { typeof(Circle<T>), typeof(Square<T>) };
    }
}


Еще одним способом задания обобщенных известных типов является применение конфигурационного файла приложения следующим образом:

<system.runtime.serialization>
  <dataContractSerializer>
    <declaredTypes>
      <add type = "Server.Shape2`1, Server">
        <knownType type = "Server.Circle2`1, Server">
          <parameter index="0"/>
        </knownType>
        <knownType type = "Server.Square2`1, Server">
          <parameter index="0"/>
        </knownType>
      </add>
    </declaredTypes>
  </dataContractSerializer>
</system.runtime.serialization>

Выводы

Разработчики WCF предусмотрели множество способов задания известных типов, каждый из которых имеет свои преимущества и недостатки. Для наиболее простых приложений самым простым способом является применение атрибута KnownTypeAttribute к базовому классу иерархии; для более сложных способов лучше воспользоваться атрибутом ServiceKnownTypeAttribute либо применять декларативный способ задания известных типов с помощью конфигурационного файла. Передача известных типов в конструкторе DataContractSerializer является наиболее трудоемкой, но позволяет не только задавать известные типы, но и перейти к NetDataContractSerializer в случае невозможности заранее определить динамические типы объектов, передаваемые в операциях контракта.

Известные типы в WCF. Часть 3

Использование конструктора класса DataContractSerializer

Очередным способом задания известных типов является использование соответствующего параметра конструктора класса DataContractSerializer. С одной стороны может показаться, что это наиболее простой способ задания известных типов, ведь для этого нужно всего лишь заполнить массив известных типов и передать этот массив в конструктор. Вот пример, который можно найти в MSDN:

public static void Constructor2()
{
    // Create a generic List of types and add the known types
    // to the collection.
    List<Type> knownTypeList = new List<Type>();
    knownTypeList.Add(typeof(PurchaseOrder));
    knownTypeList.Add(typeof(PurchaseOrderV3));
 
    // Create a DatatContractSerializer with the collection.
    DataContractSerializer ser2 = new DataContractSerializer(
        typeof(Orders), knownTypeList);
 
    // Other code not shown.
}
 
Но остается вопрос в том, как этот способ можно применить на практике. Разработчики WCF предусмотрели множество способов настройки и расширения функциональных возможностей операций, контрактов и служб, для этого предусмотрены интерфейсы IOperationBehavior, IContractBehavior и IServiceBehavior соответственно. Каждый из этих интерфейсов имеют одинаковую функциональность и могут применяться как для сервиса, так для клиента. Существует два способа применения этих интерфейсов на практике: путем программного добавления объектов, реализующих соответствующий интерфейс перед открытием хоста (на стороне сервиса) или созданием прокси (на стороне клиента), либо путем использования пользовательских атрибутов (для этого класс, реализующий один из вышеперечисленных интерфейсов должен быть наследником класса System.Attribute). Наиболее простым способом является создания класса, реализующего все вышеперечисленные интерфейсы, а также являющимся наследником от System.Attribute, что позволит применять его любым способом. Идея реализации сводится к замене поведения с типом DataContractSerializerOperationBehavior на объект собственного класса, возвращающий необходимый сериализатор.
 
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | 
                AttributeTargets.Interface)]
public class ShapeKnownTypeAttribute : Attribute, 
       IOperationBehavior, IServiceBehavior, IContractBehavior
{
    void IOperationBehavior.AddBindingParameters(OperationDescription description, 
       BindingParameterCollection parameters)
    {}
 
    void IOperationBehavior.ApplyClientBehavior(OperationDescription description, 
       System.ServiceModel.Dispatcher.ClientOperation proxy)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.ApplyDispatchBehavior(OperationDescription description, 
       System.ServiceModel.Dispatcher.DispatchOperation dispatch)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.Validate(OperationDescription description)
    {}
 
 
    void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
       ServiceHostBase serviceHostBase,
       System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints,
       BindingParameterCollection bindingParameters)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }
 
    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
       ServiceHostBase serviceHostBase)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }
 
    void IServiceBehavior.Validate(ServiceDescription serviceDescription,
       ServiceHostBase serviceHostBase)
    {}
 
    void IContractBehavior.AddBindingParameters(ContractDescription contractDescription,
       ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {}
 
    void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription,
       ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }
 
    void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription,
       ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }
 
    void IContractBehavior.Validate(ContractDescription contractDescription,
       ServiceEndpoint endpoint)
    {}
 
 
    private static void ReplaceDataContractSerializerOperationBehavior(ServiceDescription description)
    {
        foreach (var endpoint in description.Endpoints)
        {
            ReplaceDataContractSerializerOperationBehavior(endpoint);
        }
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(ContractDescription description)
    {
        foreach (var operation in description.Operations)
        {
            ReplaceDataContractSerializerOperationBehavior(operation);
        }
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(ServiceEndpoint endpoint)
    {
        // ignore mex
        if (endpoint.Contract.ContractType == typeof(IMetadataExchange))
        {
            return;
        }
        ReplaceDataContractSerializerOperationBehavior(endpoint.Contract);
    }
 
    private static void ReplaceDataContractSerializerOperationBehavior(OperationDescription description)
    {
        var behavior = description.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (behavior != null)
        {
            description.Behaviors.Remove(behavior);
            description.Behaviors.Add(new ShapeDataContractSerializerOperationBehavior(description));
        }
 
    }
 
    public class ShapeDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
    {
        public ShapeDataContractSerializerOperationBehavior(OperationDescription description)
            : base(description) { }
 
        public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
        {
            var shapeKnownTypes = new List<Type> { typeof(Circle), typeof(Square) };
            return new DataContractSerializer(type, name, ns, shapeKnownTypes);
        }
 
        public override XmlObjectSerializer CreateSerializer(Type type, 
XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
        {
            var shapeKnownTypes = new List<Type> { typeof(Circle), typeof(Square) };
            return new DataContractSerializer(type, name, ns, shapeKnownTypes);
        }
    }
 
}
Применение созданного класса следующее:
//На стороне сервиса
[ServiceContract()]
[ShapeKnownType]
public interface IService {...}
 
//На стороне клиента
[GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[ServiceContractAttribute(ConfigurationName="ServiceReference.IService"]
[Server.ShapeKnownType] //ДАННЫЙ АТРИБУТ ДОБАВЛЕН В УЖЕ СГЕНЕРИРОВАННЫЙ КОД!
public interface IService { ... }
 
Применение класса ShapeKnownType к операциям является аналогичным.
 
ПРИМЕЧАНИЕ
 
Использование объектов, реализующих один из вышеперечисленных интерфейсов на стороне сервиса не добавляет информацию об известных типах в метаданные сервиса, поэтому необходимо использование общих типов между клиентом и сервисом, и требует симметричного применения соответствующих атрибутов на стороне клиента.
 
Несмотря на то, что данный способ является самым трудоемким и может потребовать модификацию сгенерированного кода на стороне клиента, он показывает, каким образом можно изменять поведение инфраструктуры WCF и настраивать его по своему усмотрению. Именно таким способом осуществляется применение суррогатных типов (Surrogate Types), применение NetDataContractSerializer вместо DataContractSerializer, применение собственных сериализаторов и решение многих других задач расширения функциональности WCF.

Известные типы в WCF. Часть 2

Применение атрибута ServiceKnownTypeAttribute

Определение класса ServiceKnownTypeAttribute следующее:
 
[AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Method|
AttributeTargets.Interface, Inherited = true, 
    AllowMultiple = true)]
public sealed class ServiceKnownTypeAttribute : Attribute
{
  public ServiceKnownTypeAttribute(Type type);
  public ServiceKnownTypeAttribute(string methodName);
  public ServiceKnownTypeAttribute(string methodName, Type declaringType);
  //Остальные члены
}
 
Атрибут ServiceKnownTypeAttribute применяется для конкретной операции или контракту целиком.
 
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IService
{
    [OperationContract]
    [ServiceKnownType(typeof(Circle))]
    [ServiceKnownType(typeof(Square))]
    Shape CreateShape(ShapeType shapeType, int id);
 
    //Такого поведения нельзя добиться с применением атрибута KnownTypeAttribute 
    [OperationContract]
    [ServiceKnownType(typeof(Shape))]
    [ServiceKnownType(typeof(Circle))]
    [ServiceKnownType(typeof(Square))]
    void ProcessShape(object shape);
}
 
[ServiceContract(SessionMode = SessionMode.Required)]
[ServiceKnownType(typeof(Circle))]
[ServiceKnownType(typeof(Square))]
public interface IService
{
    [OperationContract]
    Shape CreateShape(ShapeType shapeType, int id);
}
 
У этого способа есть несколько особенностей. Во-первых, хотя можно применить атрибут ServiceKnownType к отдельному методу, на самом деле эффект будет такой же, как и от применения этого атрибута к контракту целиком, т.е. будет затронут не только этот метода, а все методы контракта, в которых участвует соответствующий базовый класс. Во-вторых, если не будет применяться совместное использование типов между клиентом и сервером, то приведенные примеры приведут к генерации класса Shape с атрибутами KnownTypeAttribute, а не к применению атрибута ServiceKnownTypeAttribute к сгенерированному контракту. Остальные версии конструктор ServiceKnownTypeAttribute используются для указания метода, возвращающего перечень известных типов. Один из конструкторов принимает строковый параметр и тип.
 
[ServiceContract]
[ServiceKnownType("GetKnownTypes", typeof(Helper))]
public interface IService 
{
    [OperationContract]
    Shape CreateShape(ShapeType shapeType, int id);
}
static class Helper 
{
    static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
        return new Type[] { typeof(Circle), typeof(Square) };
    }
}
 
Другой конструктор принимает только строку с именем метода и может применяться напрямую к классу службы (а не интерфейсу службы).
 
[ServiceContract]
[ServiceKnownType("GetKnownTypes")]
public class Service 
{
    static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
        return new Type[] { typeof(Circle), typeof(Square) };
    }
 
    [OperationContract]
    public Shape CreateShape(ShapeType shapeType, int id) { ... }
}
 
ПРИМЕЧАНИЕ
В отличие от атрибута KnownTypeAttribute метод, имя которого передается атрибуту ServiceKnownType, должен содержать параметр ICustomAttributeProvider. Кроме того, при попытке использовать метод с неверной сигнатурой вы получите исключение при попытке открытия службы, а не во время обновления метаданных службы клиентом

Использование конфигурационного файла приложения

Задание перечня известных типов с помощью атрибутов подразумевает, что вы заранее знаете, какие типы будут передаваться между клиентом и службой. В противном случае, понадобиться постоянная перекомпиляция, развертывание и обновление метаданных, для поддержания перечня известных типов в актуальном состоянии. Альтернативой может служить декларативное объявление известных типов в конфигурационном файле приложения сервиса. Для этого необходимо добавить информацию об известных типах в конфигурационный файл приложения или web.config:
 
  <system.runtime.serialization>
    <dataContractSerializer>
      <declaredTypes>
        <add type = "Server.Shape, Server">
          <knownType type = "Server.Circle, Server"/>
          <knownType type = "Server.Square, Server"/>
        </add>
      </declaredTypes>
    </dataContractSerializer>
  </system.runtime.serialization>
 
ПРИМЕЧАНИЕ
Для сборок со строгим именем (strongly named assemblies) строка, содержащая описание типа, помимо полного имени типа и названия сборки должна содержать номер версии, идентификатор регионального стандарта и маркер открытого ключа. Пример использования класса EventArgs из сборки mscorlib будет следующим: add type="System.EventArgs, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
 
В случае применения конфигурационного файла на стороне сервиса, известные типы будут добавляться в экспортируемые метаданные службы, и будут влиять на генерацию кода на стороне клиента. При совместном использовании типов между сервисом и клиентом в сгенерированном коде будет добавлен атрибут ServiceKnownTypeAttribute для всех методов, аргументы которых добавлены в перечень известных типов в конфигурационном файле сервиса.
 
[GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[ServiceContractAttribute(ConfigurationName="ServiceReference.IService")]
public interface IService {
   [OperationContractAttribute(Action="http://tempuri.org/IService/CreateShape", 
ReplyAction="http://tempuri.org/IService/CreateShapeResponse")]
    [ServiceKnownTypeAttribute(typeof(Server.Circle))]
    [ServiceKnownTypeAttribute(typeof(Server.Square))]
    Server.Shape CreateShape(Server.ShapeType shapeType, int id);
}
 
При отсутствия совместного использования типов, будет использован атрибут KnownTypeAttribute для базового класса иерархии:
 
[DebuggerStepThroughAttribute()]
[GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[DataContractAttribute(Name="Shape", 
Namespace="http://schemas.datacontract.org/2004/07/Server")]
[SerializableAttribute()]
[KnownTypeAttribute(typeof(Client.ServiceReference.Circle))]
[KnownTypeAttribute(typeof(Client.ServiceReference.Square))]
public partial class Shape : object, IExtensibleDataObject, 
INotifyPropertyChanged { }
 
Известные типы могут добавляться не только в конфигурационный файл сервиса, но и в конфигурационный файл клиентского приложения, но в этом случае предполагается совместное использование типом между клиентом и сервисом.

Известные типы в WCF. Часть 1

Введение

Windows Communication Foundation (WCF) - это реализация компанией Microsoft индустриального стандарта создания сервис-ориентированных (Service-Oriented) распределенных приложений. Основное отличие сервисов от обычных приложений заключается в том, что сервисы экспортируют своим клиентам только интерфейс, а реализация остается на совести самого сервиса. Этот подход далеко не нов и является логическим развитием идей объектно-ориентированного, а затем и компонентного программирования, и заключается в более строгом отделении интерфеса от реализации. Каждый сервис предоставляет своим клиентам некоторый контракт и взаимодействие между ними осуществляется путем обмена сообщениями. Это упрощает развертывание, управлениями версиями, а также позволяет реализовывать сервис и клиентов сервиса на разных операционных системах, с помощью разных технологий и на разных языках программирования. Необходимость в таком подходе обусловлена разнородностью программного обеспечения в корпоративном секторе. Крупные компании могут обладать огромным множеством различного программного обеспечения, разработанного разными производителями, на разных аппаратных и программных платформах. И когда возникает вопрос интеграции всех этих систем на помощь приходят сервисы, построенные на основе SOAP и Xml Infoset. Но такая архитектура помимо явных преимуществ содержит и некоторые недостатки. Поскольку сервис и клиент должны четко согласовывать свои интерфейсы, это налагает жесткие ограничения на типы сущностей, которые могут передаваться между ними и при этом обязательно, чтобы статический и динамический типы аргументов, возвращаемых значений, полей и свойств других сущностей совпадали. Для обхода этого ограничения разработчики предусмотрели механизмы указания перечня <известных типов> (Known Types), о которых и пойдет речь в этой статье.

Проблема передачи полиморфных объектов

Одним из основных преимуществ объектно-ориентированного программирования, к которому мы все уже давно привыкли, является использование принципа полиморфизма, который заключается в возможности передачи объектов производных классов в методы, принимающие объекты базового класса. И хотя это совершенно естественное поведение для современных языков программирования, такое поведение невозможно в мире сервис-ориентированного программного обеспечения. Рассмотрим пример. Предположим, есть некоторая иерархия объектов (классический пример с иерархией классов Shape):
 
public enum ShapeType
{
    Circle,
    Square,
}
 
[DataContract]
public abstract class Shape { ... }
 
[DataContract]
public class Circle : Shape { ... }
 
[DataContract]
public class Square : Shape { ... }
 
И есть некоторый простой сервис, реализующий фабричный метод:
 
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IService
{
    [OperationContract]
    Shape CreateShape(ShapeType shapeType, int id);
}
 
ПРИМЕЧАНИЕ
К этому примеру нельзя относиться, как к реальному примеру функциональности служб. Такая низкоуровневая абстракция, как фабричный метод - это слишком мелкая абстракция для сервиса; обычно метод сервиса должен выполнять более сложную единицу работу, чем простое полиморфное создание объекта.
 
При попытке вызова метода CreateShape, клиент получит следующую ошибку: "Тип "Server.Circle" с именем контракта данных "Circle:http://schemas.datacontract.org/2004/07/Server" не ожидается. Добавьте любые статически неизвестные типы в список известных типов - например, используя атрибут KnownTypeAttribute, или путем их добавления в список известных типов, передаваемый в DataContractSerializer." Такое поведение обусловлено архитектурными особенностями WCF, связанными с принципами построения сервис-ориентированного программного обеспечения. Клиент знает о сервисе ровно столько, сколько описано в его контракте. А поскольку контракт определяет только статический тип объекта, то клиент просто не знает о существовании других производных типах. Контракт сервиса не зависит от технологии реализации и в процессе взаимодействия клиента и сервиса происходит преобразование данных специфических для сервиса (и его технологии реализации) в нейтральный набора данных, которые уже передаются по сети, а на клиентской стороне происходит процесс преобразование нейтрального набора данных в данные, специфические для клиента. В процессе этого обратного преобразования десериализатор анализирует, какому CLR-типу (если клиент написан на WCF) соответствует тип, указанный в контракте сообщения, при этом учитывается набор известных типов десериализатора (deserializer's set of "known types"). Поэтому, для устранения такой ошибки необходимо указать десериализатору список известных типов одним из способов, предусмотренным архитектурой WCF.

Применение атрибута KnownTypeAttribute

Самым простым способом указать известные типы является использование атрибута KnownTypeAttribute, определенного следующим образом:
 
[AttributeUsage(AttributeTargets.Struct|AttributeTargets.Class,
                AllowMultiple = true)]
public sealed class KnownTypeAttribute : Attribute
{
   public KnownTypeAttribute(Type type);
   public KnownTypeAttribute(string methodName);
   //остальные члены 
}
 
Конструктор класса KnownTypeAttribute имеет две перегруженные версии. Первый конструктор принимает в качестве параметра тип, который будет добавлен в список известных типов при десериализации (при этом этот атрибут может быть применен множество раз):
 
[DataContract]
[KnownType(typeof(Square))]
[KnownType(typeof(Circle))]
public abstract class Shape { ... }
 
Второй конструктор принимает в качестве параметра строку, содержащую имя метода. Тип возвращаемое значение метода должен относится к классу IEnumerable или одному из его наследников, метод должен быть статическим и не принимать никаких параметров. При этом видимость этого метода не имеет значения.
 
[DataContract]
[KnownType("GetKnownTypes")]
public abstract class Shape
{
    static IEnumerable<Type> GetKnownTypes()
    {
        return new Type[] { typeof(Square), typeof(Circle) };
    }
}
 
Применение этого атрибута (любого конструктора) приводит к включению всех перечисленных производных классов в метаданные службы, что позволит получить на стороне клиента собственные представления необходимых классов, и даст возможность применять производные классы вместо базовых во всех контрактах и операциях. Использование атрибута KnownTypeAttribute очень простое, но имеет определенные ограничения. Подразумевается, что существует некоторая иерархия объектов, базовый класс которой передается в качестве параметра метода, возвращается в качестве возвращаемого значения или используется в качестве поля или свойства некоторого составного объекта. Но бывают случаи, в которых используется объекты класса Object явно в качестве параметра или возвращаемого значения, либо неявно при передаче таких объектов как ArrayList. Для этих целей необходимо использовать атрибут ServiceKnownTypeAttribute.
 
ПРИМЕЧАНИЕ
Если вы не используете общие типы между сервисом и клиентом (в ServiceReference не выставлен признак "Reuse type in referenced assemblies"), то после добавления атрибута KnownTypeAttribute в сервисе, необходимо обновить метаданные сервиса на клиенте.
 
ПРЕДУПРЕЖДЕНИЕ
При использовании метода, имя которого передается в атрибуте KnownTypeAttribute, с неверной сигнатурой, вы получите ошибку не при активации службы, а в момент обновления метаданных с помощью Metadata Exchange Endpoint. При обновлении метаданных из Visual Studio вы получите загадочную ошибку следующего вида: "Метаданные содержат неразрешимую ссылку: 'net.tcp://localhost:6101/WCFTestService/mex'. Более подробная информация может быть получена при генерации клиентской части с помощью SvcUtil.exe, в таком случае ошибка будет следующей: "Атрибут KnownTypeAttribute типа "Server.Shape" указывает метод с именем "GetKnownTypes" для предоставления известных типов. Статический метод "GetKnownTypes()" на этом типе не найден. Убедитесь, что метод существует и отмечен как статический."