Введение
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()" на этом типе не найден. Убедитесь, что метод существует и отмечен как статический."
Спасибо за статью. Очень полезно.
ОтветитьУдалить