понедельник, 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, и хотя данные сериализованные первым сериализатором могут быть десериализированы вторым, в обратном направлении это работает не всегда.

Вся инфраструктура WCF построена таким образом, чтобы отделить код сервиса от таких «мирских» проблем, как адреса, привязки и контракты (те самые ABC, Address-Binding-Contract), так почему бы не абстрагироваться и от типа сериализатора? Тем более, что это не настолько сложно;)

ПРИМЕЧАНИЕ
Ради читаемости, я не буду приводить код целиком. Код всех нужных классов целиком находится в конце статьи, кроме того, вы можете скачать архив солюшена целиком и посмотреть, что в нем да как.

Во-первых, давайте снова опишем ту же самую иерархию классов, с которыми мы работали в предыдущих частях:

[DataContract]
public abstract class Shape { ... }

[DataContract]
public class Square : Shape { ... }

[DataContract]
public class Circle : Shape { ... }

И интерфейс сервиса:

[ServiceContract(SessionMode = SessionMode.Required)]
public interface IShapeService
{

    [OperationContract]
    void ProcessShape(Shape shape);

    [OperationContract]
    void Foo(int i);
}

Итак, напомню, что в сервис-ориентированной архитектуре вообще, и в WCF в частности нет некоторых вещей, привычных «объектно-ориентированному» программисту. Так, WCF не поддерживает перегрузку методов (ну, это не совсем фича ООП, но однозначно весьма привычная), а также напрочь отсутствует полиморфизм; т.е. вы не можете сделать функцию, которая принимает объект базового класса и «подсовывать» в нее объекты производных классов в случае необходимости. Принципы SOA гласят, что вы должны четко специфицировать контракт, включая точный тип объектов, которые будут передаваться или приниматься методом. Если же вы все же хотите воспользоваться полиморфизмом, то должны явно указать, какие классы наследники могут быть использованы там, где вы передаете (или возвращаете) базовый тип.

Теперь давайте вернемся к нашим баранам сервисам. В наш тестовый сервис IShapeService я специально добавил функцию void Foo(int), которая должна успешно выполняться не зависимо от «известности» типов, в то время как функция void ProcessShape(Shape) принимает в качестве параметра экземпляр базового класса Shape, и без дополнительных действий будет завершаться с ошибкой при попытке передать туда экземпляр класса наследника.

Данный способ решения сводится к созданию класса наследника от BehaviorExtensionElement, и с помощью лома и какой-то матери простого кода заставить его возвращать NetDataContractSerializer вместо привычного DataContractSerializer. Затем останется только прописать его использование в конфигурационных файлах клиента и сервиса (да, это единственное изменение, которое нужно сделать, для того, чтобы существующие сервисы начали использовать NetDataContractSerializer).

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

Следующий код должен быть расположен в отдельной сборке и эта сборка должна быть доступна клиенту и серверу.

/// <summary>
/// Переопределяем DataContractSerializerOperationBehavior, чтобы вместо
/// DataContractSerializer-а возвращать NetDataContractSerializer
/// </summary>
public class NetDataContractSerializerOperationBehavior 
    : DataContractSerializerOperationBehavior
{
    public NetDataContractSerializerOperationBehavior(
                        OperationDescription operationDescription)
        : base(operationDescription)
    {}

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

/// <summary>
/// Именно этот элемент нужно будет прописать в конфигурационных файлах для использования
/// NetDataContractSerializer-а вместо DataContractSerializer-а
/// </summary>
public class NetDataContractSerializerElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(NetDataContractSerializerBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new NetDataContractSerializerBehavior();
    }
}

/// <summary>
/// Еще один вспомогательный класс, для переопределения поведения
/// </summary>
public class NetDataContractSerializerBehavior : Attribute,
    IServiceBehavior, IEndpointBehavior
{
    #region IServiceBehavior
    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {}

    public void AddBindingParameters(ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase,
            Collection<ServiceEndpoint> endpoints,
            BindingParameterCollection bindingParameters)
    {}

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase)
    {
        foreach (var endpoint in serviceDescription.Endpoints)
            RegisterContract(endpoint);
    }
    #endregion

    #region IEndpointBehavior
    public void Validate(ServiceEndpoint endpoint)
    {}

    public void AddBindingParameters(ServiceEndpoint endpoint,
            BindingParameterCollection bindingParameters)
    {
        RegisterContract(endpoint);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
            EndpointDispatcher endpointDispatcher)
    {}

    public void ApplyClientBehavior(ServiceEndpoint endpoint,
            ClientRuntime clientRuntime)
    {}
    #endregion

    #region Support
    protected void RegisterContract(ServiceEndpoint endpoint)
    {
        // Нужно заменить все зарегистрированные DataContractSerializerOperationBehavior
        // на NetDataContractSerializerOperationBehavior
        foreach (OperationDescription desc in endpoint.Contract.Operations)
        {
            var dcsOperationBehavior = desc.Behaviors.
                        Find<DataContractSerializerOperationBehavior>();
            if (dcsOperationBehavior != null)
            {
                int idx = desc.Behaviors.IndexOf(dcsOperationBehavior);
                desc.Behaviors.Remove(dcsOperationBehavior);
                desc.Behaviors.Insert(idx,
                    new NetDataContractSerializerOperationBehavior(desc));
            }
        }
    }
    #endregion
}

Теперь все, что осталось сделать, это добавить пару секций в конфигурационные файлы клиента и сервиса.

Конфиг сервиса:

<services>
  <service name="PlayingWithNetDataContractSerializer.ShapeService">
    <endpoint binding="netTcpBinding" 
              behaviorConfiguration="netDataContractSerializerEndpointBehavior"
              name="NetTcpBindingEndpoint" 
              contract="PlayingWithNetDataContractSerializer.IShapeService">
    </endpoint>
    <host>
      <baseAddresses>
        <add baseAddress="net.tcp://localhost:6101/PlayingWithNetDataContractSerializer" />
      </baseAddresses>
    </host>
  </service>
</services>

<behaviors>
  <endpointBehaviors>
    <behavior name="netDataContractSerializerEndpointBehavior">
      <!--Закомментируйте эту строку и вы снова получите ошибку во время выполнения,
      в которой будет говорится о необходимости задания известных типов-->
      <NetDataContractSerializerBehavior/>
    </behavior>
  </endpointBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions>
    <add
    name="NetDataContractSerializerBehavior"
    type="PlayingWithNetDataContractSerializer.NetDataContractSerializerElement, NetDataContractSerialializerBehavior, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  </behaviorExtensions>
</extensions>

Конфиг клиента:

<client>
  <!--bindingConfiguration="netTcpBindingEndpointConfig"-->
  <endpoint address="net.tcp://localhost:6101/PlayingWithNetDataContractSerializer" 
            binding="netTcpBinding" 
            behaviorConfiguration="netDataContractSerializerEndpointBehavior"
            contract="PlayingWithNetDataContractSerializer.IShapeService"
            name="netTcpBindingEndpoint" />
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="netDataContractSerializerEndpointBehavior">
      <!--Закомментируйте эту строку и вы снова получите ошибку во время выполнения,
      в которой будет говорится о необходимости задания известных типов-->
      <NetDataContractSerializerBehavior/>
    </behavior>
  </endpointBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions>
    <add name="NetDataContractSerializerBehavior"
          type="PlayingWithNetDataContractSerializer.NetDataContractSerializerElement, NetDataContractSerialializerBehavior, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  </behaviorExtensions>
</extensions>

Вот и все! Теперь вы можете переключаться с одного сериализатора на другой «декларативно», без изменения кода сервиса. Кроме того, обратите внимание, что если вы уберете использование поведения netDataContractSerializerEndpointBehavior из секции endpoint (или закомментируете NetDataContractSerializerBehavior из секции endpointBehaviors) конфигурационного файла клиента или сервиса, то данный тестовый пример перестанет работать и начнет падать с «просьбой» добавить каким-либо способом класс Circle (экземпляр которого мы пытаемся передать) в перечень известных типов.

Солюшен целиком.

Комментариев нет:

Отправить комментарий