Суррогаты контрактов данных

Суррогат контракта данных — это расширенная функция, основанная на модели контракта данных. Эта возможность предназначена для настройки и подстановки типов, когда необходимо изменить способ сериализации типа, десериализации или преобразования типа в метаданные. Например, суррогат может использоваться в сценариях, когда для типа не задан контракт данных, поля и свойства не помечены атрибутом DataMemberAttribute или пользователи хотят динамически создавать вариации схемы.

Сериализация и десериализация выполняются с суррогатом контракта данных при использовании DataContractSerializer для преобразования из .NET Framework в подходящий формат, например XML. Суррогат контракта данных также может использоваться для изменения метаданных, экспортированных для типов, при создании представлений метаданных, например документов схемы XML (XSD). Во время импорта из метаданных создается код, суррогат может также использоваться для настройки создаваемого кода.

Как работает суррогат

Суррогат сопоставляет один тип ("исходный" тип) с другим типом ("суррогатным" типом). В следующем примере показаны исходный тип Inventory и новый суррогатный тип InventorySurrogated. Тип Inventory не позволяет сериализацию, а тип InventorySurrogated позволяет:

public class Inventory
{
    public int pencils;
    public int pens;
    public int paper;
}

Так как для этого класса контракт данных не определен, класс преобразуется в суррогатный класс с контрактом данных. Суррогатный класс показан в следующем примере:

[DataContract(Name = "Inventory")]
public class InventorySurrogated
{
    [DataMember]
    public int numpencils;
    [DataMember]
    public int numpaper;
    [DataMember]
    private int numpens;

    public int pens
    {
        get { return numpens; }
        set { numpens = value; }
    }
}

Реализация IDataContractSurrogate

Для использования суррогата контракта данных необходимо реализовать интерфейс IDataContractSurrogate.

Ниже приведен обзор каждого метода интерфейса IDataContractSurrogate с возможной реализацией.

GetDataContractType

Метод GetDataContractType сопоставляет один тип с другим. Этот метод требуется для сериализации, десериализации, импорта и экспорта.

Первая задача - определить, какие типы будут сопоставляться с другими типами. Например:

public Type GetDataContractType(Type type)
{
    Console.WriteLine("GetDataContractType");
    if (typeof(Inventory).IsAssignableFrom(type))
    {
        return typeof(InventorySurrogated);
    }
    return type;
}
  • При сериализации сопоставление, которое возвращает этот метод, используется затем для преобразования исходного экземпляра в суррогатный экземпляр вызовом метода GetObjectToSerialize.

  • При десериализации сопоставление, которое возвращает этот метод, используется сериализатором для десериализации в экземпляр суррогатного типа. Затем вызывается метод GetDeserializedObject для преобразования суррогатного экземпляра в экземпляр исходного типа.

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

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

Параметр Type является типом объекта, который подвергается сериализации, десериализации, импорту или экспорту. Метод GetDataContractType должен возвращать тип, который был на входе, если суррогат не обрабатывает тип. В противном случае возвращается соответствующий суррогатный тип. Если существует несколько суррогатных типов, метод позволяет определить несколько сопоставлений.

Метод GetDataContractType не вызывается для встроенных примитивов контрактов данных, например Int32 или String. Для других типов, таких как массивы, типы, определяемые пользователем, и других структур данных, метод вызывается для каждого типа.

В предыдущем примере метод проверяет, совместим ли параметр type и Inventory. Если да, то метод сопоставляет его с InventorySurrogated. Каждый раз при сериализации, десериализации, импорте схемы или экспорте схемы сперва вызывается эта функция для определения соответствия типов.

Метод GetObjectToSerialize

Метод GetObjectToSerialize преобразует экземпляр исходного типа в экземпляр суррогатного типа. Этот метод требуется для сериализации.

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

public object GetObjectToSerialize(object obj, Type targetType)
{
    Console.WriteLine("GetObjectToSerialize");
    if (obj is Inventory)
    {
        InventorySurrogated isur = new InventorySurrogated();
        isur.numpaper = ((Inventory)obj).paper;
        isur.numpencils = ((Inventory)obj).pencils;
        isur.pens = ((Inventory)obj).pens;
        return isur;
    }
    return obj;
}

Метод GetObjectToSerialize вызывается при сериализации объекта. Этот метод передает данные из исходного типа в поля суррогатного типа. Поля могут быть напрямую сопоставлены с суррогатными полями, либо в суррогате можно сохранить результат манипуляций с исходными данными. Некоторые варианты использования: прямое сопоставление полей; выполнение операций над данными, которые нужно сохранить в суррогатных полях; сохранение XML исходного типа в суррогатном поле.

Параметр targetType относится к объявленному типу элемента. Этот параметр является суррогатным типом, возвращенным методом GetDataContractType. Сериализатор не требует, чтобы возвращенный объект можно было назначить этому типу. Параметр obj является объектом для сериализации и преобразуется в его суррогат при необходимости. Этот метод возвращает входной объект, если суррогатный не обрабатывает этот объект. В противном случае возвращается новый суррогатный объект. Суррогат не вызывается, если объект пустой. Метод позволяет определить несколько суррогатных сопоставлений для различных экземпляров.

При создании объекта DataContractSerializer можно задать для него сохранение ссылок на объекты. (Дополнительные сведения см. в разделе Сериализация и десериализация.) Это делается путем задания preserveObjectReferences параметра в конструкторе true. В таком случае суррогат вызывается для объекта только один раз, так как все последующие сериализации просто записывают в поток ссылку. Если параметр preserveObjectReferences имеет значение false, суррогат вызывается для каждого вхождения экземпляра.

Если тип экземпляра, подвергающегося сериализации, отличается от объявленного типа, сведения о типе записываются в поток (например xsi:type), чтобы экземпляр затем можно было десериализовать. Это происходит независимо от того, является объект суррогатным, или нет.

В приведенном выше примере данные экземпляра Inventory преобразуются в данные InventorySurrogated. Проверяется тип объекта и выполняются необходимые операции для преобразования в суррогатный тип. В данном случае поля класса Inventory напрямую копируются в поля класса InventorySurrogated.

Метод GetDeserializedObject

Метод GetDeserializedObject преобразует экземпляр суррогатного типа в экземпляр исходного типа. Этот метод требуется для десериализации.

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

public object GetDeserializedObject(object obj, Type targetType)
{
    Console.WriteLine("GetDeserializedObject");
    if (obj is InventorySurrogated)
    {
        Inventory invent = new Inventory();
        invent.pens = ((InventorySurrogated)obj).pens;
        invent.pencils = ((InventorySurrogated)obj).numpencils;
        invent.paper = ((InventorySurrogated)obj).numpaper;
        return invent;
    }
    return obj;
}

Этот метод вызывается только во время десериализации объекта. Он предоставляет возможность обратного сопоставления данных для десериализации из суррогатного типа обратно в исходный тип. Аналогично методу GetObjectToSerialize, возможен прямой обмен данными полей, выполнение операций над данными или хранение XML-данных. При десериализации не всегда получаются данные, идентичные исходным, что связано с операциями при преобразовании данных.

Параметр targetType относится к объявленному типу элемента. Этот параметр является суррогатным типом, возвращенным методом GetDataContractType. Параметр obj ссылается на объект, который был десериализирован. Объект может быть преобразован обратно в свой исходный тип, если он был заменен на суррогатный. Этот метод возвращает входной объект, если суррогатный не обрабатывает этот объект. В противном случае после завершения преобразования возвращается десериализованный объект. Если есть несколько суррогатных типов, можно задать преобразование данных из суррогатного типа в исходный для каждого из них, указав каждый тип и его преобразование.

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

В предшествующем примере объекты типа InventorySurrogated преобразуются обратно в исходный тип Inventory. В данном случае данные напрямую передаются из полей InventorySurrogated в соответствующие поля Inventory. Так как операций с данными не производилось, все поля элементов будут содержать те же данные, что и до сериализации.

Метод GetCustomDataToExport

При экспорте схемы метод GetCustomDataToExport является необязательным. Он применяется для вставки дополнительных данных или указаний в экспортированную схему. Дополнительные данные могут быть добавлены на уровне элемента или типа. Например:

public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
{
    Console.WriteLine("GetCustomDataToExport(Member)");
    System.Reflection.FieldInfo fieldInfo = (System.Reflection.FieldInfo)memberInfo;
    if (fieldInfo.IsPublic)
    {
        return "public";
    }
    else
    {
        return "private";
    }
}

Этот метод (с двумя перегрузками) позволяет включение дополнительных сведений в метаданные на уровне элемента или типа. Можно добавить указания о том, является ли элемент открытым или закрытым, а также комментарии, которые сохраняются при экспорте и импорте схемы. Если не применять этот метод, такая информация теряется. Этот метод не позволяет добавлять или удалять элементы и типы, а дает возможность добавлять дополнительные данные в схемы на любом из этих двух уровней.

Этот метод перегружен и может принимать либо объект Type (параметр clrtype), либо объект MemberInfo (параметр memberInfo). Второй параметр всегда будет объектом Type (параметр dataContractType). Этот метод вызывается для каждого элемента и типа суррогатного типа dataContractType.

Каждая из перегрузок возвращает либо null, либо объект, который можно сериализовать. Непустой объект сериализуется как заметка в экспортированной схеме. Для перегрузки Type каждый тип, экспортированный в схему, предается этому методу в первом параметре с суррогатным типом в качестве параметра dataContractType. Для перегрузки MemberInfo каждый элемент, экспортированный в схему, отправляет свои сведения как параметр memberInfo с суррогатным типом в качестве второго параметра.

Метод GetCustomDataToExport (Type, Type)

Метод IDataContractSurrogate.GetCustomDataToExport(Type, Type) вызывается во время экспорта схемы для каждого определения типа. Этот метод добавляет информацию в типы схемы при экспорте. Каждый определенный тип отправляется этому методу с целью определить, есть ли дополнительные данные, которые необходимо включить в схему.

Метод GetCustomDataToExport (MemberInfo, Type)

Метод IDataContractSurrogate.GetCustomDataToExport(MemberInfo, Type) вызывается во время экспорта для каждого элемента в экспортируемых типах. Эта функция дает возможность настроить для элементов любые комментарии, которые будут включены в схему при экспорте. Сведения для каждого элемента класса отправляются в этот метод для проверки, не нужно ли включить в схему какие-либо дополнительные данные.

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

Метод GetReferencedTypeOnImport

Этот метод сопоставляет тип Type суррогата с исходным типом. Этот метод является необязательным для импорта схем.

При создании суррогата, импортирующего схему и генерирующего код для нее, следующая задача - перейти от типа суррогатного экземпляра к исходному типу.

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

При импорте схемы этот метод вызывается для каждого объявления типа, чтобы сопоставить суррогатный контракт данных с типом. Строковые параметры typeName и typeNamespace определяют имя и пространство имен суррогатного типа. Возвращаемое значение для метода GetReferencedTypeOnImport используется для определения, нужно ли сгенерировать новый тип. Этот метод возвращает либо допустимый тип, либо значение NULL. В случае допустимого типа возвращаемый тип используется как тип, на который существует ссылка в сгенерированном коде. Если возвращается значение NULL, ссылки на тип не будет, должен быть создан новый тип. Если есть несколько суррогатов, можно выполнить сопоставление каждого суррогатного типа и его исходного типа.

Параметр customData является объектом, возвращенным методом GetCustomDataToExport. Параметр customData используется, когда создатели суррогата хотят поместить дополнительные данные или указания в метаданные, которые используются при импорте для генерирования кода.

Метод ProcessImportedType

Метод ProcessImportedType позволяет настроить любой тип, созданный при импорте схемы. Этот метод является необязательным.

При импорте схемы этот метод позволяет настройку любого импортированного типа и сведений о компиляции. Например:

public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
{
    Console.WriteLine("ProcessImportedType");
    foreach (CodeTypeMember member in typeDeclaration.Members)
    {
        object memberCustomData = member.UserData[typeof(IDataContractSurrogate)];
        if (memberCustomData != null
          && memberCustomData is string
          && ((string)memberCustomData == "private"))
        {
            member.Attributes = ((member.Attributes & ~MemberAttributes.AccessMask) | MemberAttributes.Private);
        }
    }
    return typeDeclaration;
}

Во время импорта этот метод вызывается для каждого генерируемого типа. Измените объект CodeTypeDeclaration или CodeCompileUnit. Это означает в том числе изменение имени, элементов, атрибутов и многих других свойств объекта CodeTypeDeclaration. Обрабатывая объект CodeCompileUnit, можно изменить директивы, пространства имен, сборки, на которые существуют ссылки, и некоторые другие аспекты.

Параметр CodeTypeDeclaration содержит объявление типа Code DOM. Параметр CodeCompileUnit позволяет изменение обработки кода. Если возвращается результат null, в объявлении типа он уничтожается. В противном случае, если возвращается объект CodeTypeDeclaration, изменения сохраняются.

Если при экспорте метаданных добавляются пользовательские данные, необходимо, чтобы они были предоставлены пользователю при импорте для их использования. Такие пользовательские данные могут использоваться для указаний о модели программирования или других комментариев. Каждый экземпляр CodeTypeDeclaration и CodeTypeMember включает в себя пользовательские данные в виде свойства UserData, приведенного к типу IDataContractSurrogate.

В приведенном выше примере в импортированной схеме производится несколько изменений. Код сохраняет закрытые элементы исходного типа при помощи суррогата. При импорте схемы модификатор доступа по умолчанию имеет значение public (открытый). Поэтому все элементы суррогатной схемы будут открытыми, если не изменить их, как в этом примере. При экспорте в метаданные вставляются пользовательские данные, указывающие, какие элементы являются закрытыми. Производится поиск в пользовательских данных и проверка, должен ли модификатор доступа быть закрытым. Затем выполняется изменение соответствующего элемента, он делается закрытым при помощи атрибутов. Без такой настройки элемент numpens был бы определен как открытый вместо закрытого.

Метод GetKnownCustomDataTypes

Этот метод получает определенные типы пользовательских данных из схемы. Этот метод является необязательным для импорта схем.

Этот метод вызывается в начале экспорта или импорта схемы. Метод возвращает типы пользовательских данных, которые используются в экспортируемой или импортируемой схеме. Методу передается объект Collection<T> (параметр customDataTypes), который представляет собой коллекцию типов. Метод добавляет дополнительные известные типы в эту коллекцию. Известные типы пользовательских данных необходимы для сериализации и десериализации пользовательских данных при помощи DataContractSerializer. Дополнительные сведения см. в разделе "Известные типы контракта данных".

Реализация суррогата

Чтобы использовать суррогат контракта данных в WCF, необходимо выполнить несколько специальных процедур.

Использование суррогата для сериализации и десериализации

Для выполнения сериализации и десериализации данных с суррогатом используйте DataContractSerializer. Объект DataContractSerializer создается при помощи DataContractSerializerOperationBehavior. Необходимо также задать суррогат.

Реализация сериализации и десериализации
  1. Создайте экземпляр ServiceHost для службы. Полные инструкции см. в разделе "Базовое программирование WCF".

  2. Для каждого объекта ServiceEndpoint заданного узла службы найдите соответствующий объект OperationDescription.

  3. Выполните поиск экземпляра DataContractSerializerOperationBehavior в поведениях операций.

  4. Если найден экземпляр DataContractSerializerOperationBehavior, задайте в его свойстве DataContractSurrogate новый экземпляр суррогата. Если экземпляр DataContractSerializerOperationBehavior не найден, создайте новый экземпляр и задайте для элемента DataContractSurrogate нового расширения новый экземпляр суррогата.

  5. Наконец, добавьте это новое поведение в текущие поведения операций, как показано в примере:

    using (ServiceHost serviceHost = new ServiceHost(typeof(InventoryCheck)))
        foreach (ServiceEndpoint ep in serviceHost.Description.Endpoints)
        {
            foreach (OperationDescription op in ep.Contract.Operations)
            {
                DataContractSerializerOperationBehavior dataContractBehavior =
                    op.Behaviors.Find<DataContractSerializerOperationBehavior>()
                    as DataContractSerializerOperationBehavior;
                if (dataContractBehavior != null)
                {
                    dataContractBehavior.DataContractSurrogate = new InventorySurrogated();
                }
                else
                {
                    dataContractBehavior = new DataContractSerializerOperationBehavior(op);
                    dataContractBehavior.DataContractSurrogate = new InventorySurrogated();
                    op.Behaviors.Add(dataContractBehavior);
                }
            }
        }
    

Использование суррогата для импорта метаданных

При импорте метаданных, таких как WSDL и XSD, для генерации клиентского кода суррогат необходимо добавить в компонент, отвечающий за генерирование кода из схемы XSD, XsdDataContractImporter. Для этого напрямую измените объект WsdlImporter, который используется для импорта метаданных.

Реализация суррогата для импорта метаданных
  1. Импортируйте метаданные при помощи класса WsdlImporter.

  2. Чтобы проверить, определен ли объект TryGetValue, воспользуйтесь методом XsdDataContractImporter.

  3. Если метод TryGetValue возвращает значение false, создайте новый объект XsdDataContractImporter и задайте в его свойстве Options новый экземпляр класса ImportOptions. В противном случае используйте импортер, возвращенный параметром out метода TryGetValue.

  4. Если для импортера XsdDataContractImporter не определено свойство ImportOptions, задайте для свойства новый экземпляр класса ImportOptions.

  5. Задайте для свойства DataContractSurrogate объекта ImportOptions импортера XsdDataContractImporter в качестве значения новый экземпляр суррогата.

  6. Добавьте объект XsdDataContractImporter в коллекцию, возвращенную свойством State объекта WsdlImporter (унаследован от класса MetadataExporter).

  7. Используйте метод ImportAllContracts объекта WsdlImporter для импорта всех контрактов данных схемы. Во время последнего действия генерируется код из схем, загруженных при вызове суррогата.

    MetadataExchangeClient mexClient = new MetadataExchangeClient(metadataAddress);
    mexClient.ResolveMetadataReferences = true;
    MetadataSet metaDocs = mexClient.GetMetadata();
    WsdlImporter importer = new WsdlImporter(metaDocs);
    object dataContractImporter;
    XsdDataContractImporter xsdInventoryImporter;
    if (!importer.State.TryGetValue(typeof(XsdDataContractImporter),
        out dataContractImporter))
        xsdInventoryImporter = new XsdDataContractImporter();
    
    xsdInventoryImporter = (XsdDataContractImporter)dataContractImporter;
    xsdInventoryImporter.Options ??= new ImportOptions();
    xsdInventoryImporter.Options.DataContractSurrogate = new InventorySurrogated();
    importer.State.Add(typeof(XsdDataContractImporter), xsdInventoryImporter);
    
    Collection<ContractDescription> contracts = importer.ImportAllContracts();
    

Использование суррогата для экспорта метаданных

По умолчанию при экспорте метаданных из WCF для службы необходимо создать схему WSDL и XSD. Суррогат необходимо добавить в компонент, отвечающий за генерацию XSD-схемы для типов контрактов данных, XsdDataContractExporter. Для этого используйте либо поведение, реализующее IWsdlExportExtension для изменения WsdlExporter, либо напрямую измените объект WsdlExporter, который используется для экспорта метаданных.

Использование суррогата для экспорта метаданных
  1. Создайте новый объект WsdlExporter или используйте параметр wsdlExporter, переданный методу ExportContract.

  2. Чтобы проверить, определен ли объект TryGetValue, воспользуйтесь функцией XsdDataContractExporter.

  3. Если метод TryGetValue возвращает значение false, создайте новый объект XsdDataContractExporter со сгенерированными схемами XML из объекта WsdlExporter, и добавьте его в коллекцию, возвращенную свойством State объекта WsdlExporter. В противном случае используйте экспортер, возвращенный параметром out метода TryGetValue.

  4. Если для экспортера XsdDataContractExporter не определен объект ExportOptions, задайте для свойства Options в качестве значения новый экземпляр класса ExportOptions.

  5. Задайте для свойства DataContractSurrogate объекта ExportOptions импортера XsdDataContractExporter в качестве значения новый экземпляр суррогата. Дальнейшие шаги по экспорту метаданных остаются без изменений.

    WsdlExporter exporter = new WsdlExporter();
    //or
    //public void ExportContract(WsdlExporter exporter,
    // WsdlContractConversionContext context) { ... }
    object dataContractExporter;
    XsdDataContractExporter xsdInventoryExporter;
    if (!exporter.State.TryGetValue(typeof(XsdDataContractExporter),
        out dataContractExporter))
    {
        xsdInventoryExporter = new XsdDataContractExporter(exporter.GeneratedXmlSchemas);
    }
    else
    {
        xsdInventoryExporter = (XsdDataContractExporter)dataContractExporter;
    }
    
    exporter.State.Add(typeof(XsdDataContractExporter), xsdInventoryExporter);
    
    if (xsdInventoryExporter.Options == null)
        xsdInventoryExporter.Options = new ExportOptions();
    xsdInventoryExporter.Options.DataContractSurrogate = new InventorySurrogated();
    

См. также