Сообщений 5    Оценка 165        Оценить  
Система Orphus

Известные типы (Known Types) в WCF

Автор: Сергей Тепляков
Источник: RSDN Magazine #3-2009
Опубликовано: 28.02.2010
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Проблема передачи полиморфных объектов
Применение атрибута KnownTypeAttribute
Применение атрибута ServiceKnownTypeAttribute
Использование конфигурационного файла приложения
Использование конструктора класса DataContractSerializer
Задание обобщенных известных типов
Выводы

Введение

В WCF сервис и клиент должны четко согласовывать свои интерфейсы. Это налагает жесткие ограничения на типы сущностей, которые могут передаваться между ними. При этом обязательно, чтобы статический и динамический типы аргументов, возвращаемых значений, полей и свойств других сущностей совпадали. Для обхода этого ограничения разработчики предусмотрели механизмы указания перечня «известных типов» (Known Types), о которых и пойдет речь в этой статье.

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

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

Рассмотрим пример. Предположим, есть некоторая иерархия объектов (классический пример с иерархией классов Shape):

      public
      enum ShapeType
{
    Circle,
    Square,
}

[DataContract]
publicabstractclass Shape { ... }

[DataContract]
publicclass Circle : Shape { ... }

[DataContract]
publicclass Square : Shape { ... }

И есть некоторый простой сервис, реализующий фабричный метод:

[ServiceContract(SessionMode = SessionMode.Required)]
publicinterface 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)]
publicsealedclass KnownTypeAttribute : Attribute
{
   public KnownTypeAttribute(Type type);
   public KnownTypeAttribute(string methodName);
   //остальные члены 
}

Конструктор класса KnownTypeAttribute имеет две перегруженные версии. Первый конструктор принимает в качестве параметра тип, который будет добавлен в список известных типов при десериализации (при этом этот атрибут может быть применен множество раз):

[DataContract]
[KnownType(typeof(Square))]
[KnownType(typeof(Circle))]
publicabstractclass Shape { ... }

Второй конструктор принимает в качестве параметра строку, содержащую имя метода. Тип возвращаемого значения метода должен реализовывать интерфейс IEnumerable<Type>, метод должен быть статическим и не принимать никаких параметров. При этом видимость этого метода не имеет значения.

[DataContract]
[KnownType("GetKnownTypes")]
publicabstractclass Shape
{
    static IEnumerable<Type> GetKnownTypes()
    {
        returnnew 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()" на этом типе не найден. Убедитесь, что метод существует и отмечен как статический.”

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

Определение класса ServiceKnownTypeAttribute следующее:

[AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Method|
AttributeTargets.Interface, Inherited = true, 
	AllowMultiple = true)]
publicsealedclass ServiceKnownTypeAttribute : Attribute
{
  public ServiceKnownTypeAttribute(Type type);
  public ServiceKnownTypeAttribute(string methodName);
  public ServiceKnownTypeAttribute(string methodName, Type declaringType);
  //Остальные члены
}

Атрибут ServiceKnownTypeAttribute применяется для конкретной операции или контракта целиком.

[ServiceContract(SessionMode = SessionMode.Required)]
publicinterface 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))]
publicinterface IService
{
    [OperationContract]
    Shape CreateShape(ShapeType shapeType, int id);
}

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

Остальные версии конструктора ServiceKnownTypeAttribute используются для указания метода, возвращающего перечень известных типов. Один из конструкторов принимает строковый параметр и тип.

[ServiceContract]
[ServiceKnownType("GetKnownTypes", typeof(Helper))]
publicinterface IService 
{
    [OperationContract]
    Shape CreateShape(ShapeType shapeType, int id);
}
staticclass Helper 
{
    static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
        returnnew Type[] { typeof(Circle), typeof(Square) };
    }
}

Другой конструктор принимает только строку с именем метода и может применяться напрямую к классу службы (а не интерфейсу службы).

[ServiceContract]
[ServiceKnownType("GetKnownTypes")]
publicclass Service 
{
    static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
        returnnew 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")]
publicinterface 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 { }

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

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

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

      public
      static
      void Constructor2()
{
    // Добавляем известные типы в список с типами
    List<Type> knownTypeList = new List<Type>();
    knownTypeList.Add(typeof(PurchaseOrder));
    knownTypeList.Add(typeof(PurchaseOrderV3));

    // Создаем объект DataContractSerializer с коллекцией известных типов
    DataContractSerializer ser2 = new DataContractSerializer(
        typeof(Orders), knownTypeList);

    // остальной код пропущен}

Но остается вопрос, как этот способ можно применить на практике.

Разработчики WCF предусмотрели множество способов настройки и расширения функциональных возможностей операций, контрактов и служб, для этого предусмотрены интерфейсы IOperationBehavior, IContractBehavior и IServiceBehavior соответственно. Эти интерфейсы имеют одинаковую функциональность и могут применяться как для сервиса, так для клиента. Существует два способа применения этих интерфейсов: путем программного добавления объектов, реализующих соответствующий интерфейс перед открытием хоста (на стороне сервиса), или созданием прокси (на стороне клиента), либо путем использования пользовательских атрибутов (для этого класс, реализующий один из вышеперечисленных интерфейсов, должен быть наследником класса System.Attribute).

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

Идея реализации сводится к замене объекта-поведения с типом DataContractSerializerOperationBehavior на объект собственного класса, возвращающий необходимый сериализатор.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | 
                AttributeTargets.Interface)]
publicclass 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)
    {}


    privatestaticvoid ReplaceDataContractSerializerOperationBehavior(
            ServiceDescription description)
    {
        foreach (var endpoint in description.Endpoints)
        {
            ReplaceDataContractSerializerOperationBehavior(endpoint);
        }
    }

    privatestaticvoid ReplaceDataContractSerializerOperationBehavior(
            ContractDescription description)
    {
        foreach (var operation in description.Operations)
        {
            ReplaceDataContractSerializerOperationBehavior(operation);
        }
    }

    privatestaticvoid ReplaceDataContractSerializerOperationBehavior(
            ServiceEndpoint endpoint)
    {
        // игнорируем mex-интерфейсif (endpoint.Contract.ContractType == typeof(IMetadataExchange))
        {
            return;
        }
        ReplaceDataContractSerializerOperationBehavior(endpoint.Contract);
    }

    privatestaticvoid ReplaceDataContractSerializerOperationBehavior(
            OperationDescription description)
    {
        var behavior = 
         description.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (behavior != null)
        {
            description.Behaviors.Remove(behavior);
            description.Behaviors.Add(
                new ShapeDataContractSerializerOperationBehavior(description));
        }

    }

    publicclass ShapeDataContractSerializerOperationBehavior 
            : DataContractSerializerOperationBehavior
    {
        public ShapeDataContractSerializerOperationBehavior(
                OperationDescription description)
            : base(description) { }

        publicoverride XmlObjectSerializer CreateSerializer(Type type, 
                string name, string ns, IList<Type> knownTypes)
        {
            var shapeKnownTypes = 
                new List<Type> { typeof(Circle), typeof(Square) };
            returnnew DataContractSerializer(type, name, ns, shapeKnownTypes);
        }

        publicoverride XmlObjectSerializer CreateSerializer(Type type, 
                XmlDictionaryString name, XmlDictionaryString ns, 
                IList<Type> knownTypes)
        {
            var shapeKnownTypes = 
                new List<Type> { typeof(Circle), typeof(Square) };
            returnnew DataContractSerializer(type, name, ns, shapeKnownTypes);
        }
    }
}

Применение созданного класса:

      //На стороне сервиса
[ServiceContract()]
[ShapeKnownType]
publicinterface IService {...}

//На стороне клиента
[GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[ServiceContractAttribute(ConfigurationName="ServiceReference.IService"]
[Server.ShapeKnownType] //ДАННЫЙ АТРИБУТ ДОБАВЛЕН В УЖЕ СГЕНЕРИРОВАННЫЙ КОД!publicinterface IService { ... }

К операциям класс ShapeKnownType применяется аналогично.

ПРИМЕЧАНИЕ

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

Несмотря на то, что данный способ является самым трудоемким и может потребовать модификации сгенерированного кода на стороне клиента, он показывает, каким образом можно изменять поведение инфраструктуры WCF и настраивать его по своему усмотрению. Именно таким способом осуществляется применение суррогатных типов (Surrogate Types), применение NetDataContractSerializer вместо DataContractSerializer, применение собственных сериализаторов и решение многих других задач расширения функциональности WCF.

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

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

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

ПРИМЕЧАНИЕ

Закрытым обобщенным типом (closed generic type) называется обобщенный тип, в котором всем аргументам-типам переданы действительные типы данных. Таким образом, List<T> является открытым типом, а List<int> – закрытым.

[DataContract]
publicclass Shape<T> { ... }

[DataContract]
publicclass Circle<T> : Shape<T> { ... }

[DataContract]
publicclass Square<T> : Shape<T> { ... }

[ServiceContract]
publicinterface 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<>))]
publicclass Shape<T> { ... }

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

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

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

[DataContract]
[KnownType("GetKnownTypes")]
publicclass Shape<T>
{
    ...
    static IEnumerable<Type> GetKnownTypes()
    {
        returnnew [] { 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 в случае невозможности заранее определить динамические типы объектов, передаваемые в операциях контракта.


Эта статья опубликована в журнале RSDN Magazine #3-2009. Информацию о журнале можно найти здесь
    Сообщений 5    Оценка 165        Оценить