Поделиться через


Задание передачи данных в контрактах служб

Windows Communication Foundation (WCF) можно рассматривать как инфраструктуру обмена сообщениями. Операции служб могут получать сообщения, обрабатывать их и отправлять. Сообщения описываются с помощью контрактов операций. Например, рассмотрим следующий контракт:

[ServiceContract]
public interface IAirfareQuoteService
{
    [OperationContract]
    float GetAirfare(string fromCity, string toCity);
}

Операция GetAirfare принимает сообщение с информацией о fromCity и toCity, затем возвращает сообщение, содержащее число.

В этом разделе рассматриваются различные способы описания сообщений в контракте операции.

Описание сообщений с помощью параметров

Простейший способ описания сообщений — с помощью списка параметров и возвращаемого значения. В приведенном выше примере строковые параметры fromCity и toCity используются для описания сообщения запроса, а возвращаемое значение типа "float" — для описания ответного сообщения. Если возвращаемое значение само по себе недостаточно для описания ответного сообщения, можно использовать выходные параметры. Например, следующая операция содержит строки fromCity и toCity в сообщении запроса и число с названием валюты в ответном сообщении:

[OperationContract]
float GetAirfare(string fromCity, string toCity, out string currency);

Кроме того, используя ссылочные параметры, можно сделать параметр частью обоих сообщений (запроса и ответа). Параметры должны принадлежать к сериализуемому типу (преобразуемому в XML). По умолчанию WCF использует компонент, называемый классом DataContractSerializer, для выполнения этого преобразования. Большинство примитивных типов (таких как int, string, float и DateTime) поддерживается. Как правило, пользовательские типы должны иметь контракт данных. Дополнительные сведения см. в разделе Использование контрактов данных.

public interface IAirfareQuoteService
{
    [OperationContract]
    float GetAirfare(Itinerary itinerary, DateTime date);
    }
    [DataContract]
    public class Itinerary
    {
        [DataMember]
        public string fromCity;
        [DataMember]
        public string toCity;
}

Для сериализации некоторых типов сериализатор DataContractSerializer непригоден. WCF поддерживает альтернативный модуль сериализации XmlSerializer, который также можно использовать для сериализации параметров. Сериализатор XmlSerializer позволяет более четко контролировать результирующий XML-код с помощью атрибутов, таких как XmlAttributeAttribute. Чтобы перейти на использование сериализатора XmlSerializer для определенной операции или для всей службы, примените к ней атрибут XmlSerializerFormatAttribute. Пример.

[ServiceContract]
public interface IAirfareQuoteService
{
    [OperationContract]
    [XmlSerializerFormat]
    float GetAirfare(Itinerary itinerary, DateTime date);
}
public class Itinerary
{
    public string fromCity;
    public string toCity;
    [XmlAttribute]
    public bool isFirstClass;
}

Дополнительные сведения см. в разделе Использование класса XmlSerializer. Следует помнить, что задавать использование модуля XmlSerializer вручную (как показано выше) не рекомендуется, если для этого нет определенных причин, рассмотренных в этом разделе.

Изолировать имена параметров .NET от имен контрактов можно с помощью атрибута MessageParameterAttribute, а задать имя контракта — с помощью свойства Name. Например, следующий контракт операции эквивалентен приведенному в первом примере этого раздела.

[OperationContract]
public float GetAirfare(
    [MessageParameter(Name="fromCity")] string originCity,
    [MessageParameter(Name="toCity")] string destinationCity);

Описание пустых сообщений

Пустое сообщение запроса можно описать с помощью отсутствия входных и ссылочных параметров. Пример.

[OperationContract]

public int GetCurrentTemperature();

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

[OperationContract]
public void SetTemperature(int temperature);

Это действие не является односторонней операцией, как например:

[OperationContract(IsOneWay=true)]
public void SetLightbulbStatus(bool isOn);

Операция SetTemperatureStatus возвращает пустое сообщение. Вместо него она может вернуть ошибку, если возникнет проблема при обработке входного сообщения. Операция SetLightbulbStatus не возвращает какое-либо значение. Эта операция никоим образом не может сообщить об ошибке.

Описание сообщений с помощью контрактов сообщений

Иногда имеет смысл представить все сообщение одним типом. Хотя для этого можно использовать контракт данных, все же рекомендуется использовать контракт сообщений: это позволяет избежать чрезмерных уровней заключения в оболочку в результирующем XML-коде. Кроме того, контракты сообщений обеспечивают более полное управление результирующими сообщениями. Например, можно задать, какие именно сведения должны находиться в тексте сообщения и какие в заголовке. В следующем примере показано использование контрактов сообщений.

[ServiceContract]
public interface IAirfareQuoteService
{
    [OperationContract]
    GetAirfareResponse GetAirfare(GetAirfareRequest request);
}

[MessageContract]
public class GetAirfareRequest
{
    [MessageHeader] public DateTime date;
    [MessageBodyMember] public Itinerary itinerary;
}

[MessageContract]
public class GetAirfareResponse
{
    [MessageBodyMember] public float airfare;
    [MessageBodyMember] public string currency;
}

[DataContract]
public class Itinerary
{
    [DataMember] public string fromCity;
    [DataMember] public string toCity;
}

Дополнительные сведения см. в разделе Использование контрактов сообщений.

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

Описание сообщений с помощью потоков

Еще один способ описания сообщений в операциях — использование класса Stream (или одного из производных от него классов) в контракте операции или в качестве элемента текста контракта сообщения (в этом случае он должен быть единственным элементом). Для входящих сообщений необходимо использовать тип Stream: производные классы использовать не допускается.

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

[OperationContract]
public Stream DownloadFile(string fileName);

Нельзя совмещать потоковые данные (Stream) с непотоковыми в одном теле сообщения. Используйте контракт сообщения, чтобы поместить дополнительные данные в заголовок сообщения. В следующем примере показано неправильное использование потоков при определении контракта операции.

//Incorrect:
// [OperationContract]
// public void UploadFile (string fileName, Stream fileData);

В следующем образце показано правильное использование потоков при определении контракта операции.

[OperationContract]
public void UploadFile (UploadFileMessage message);
//code omitted
[MessageContract]
public class UploadFileMessage
{
    [MessageHeader] public string fileName;
    [MessageBodyMember] public Stream fileData;
}

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

Использование класса сообщений

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

[OperationContract]
public void LogMessage(Message m);

Это расширенный сценарий, подробно описанный в разделе Использование класса сообщений.

Описание сообщений об ошибках

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

[OperationContract]
float GetAirfare(string fromCity, string toCity, DateTime date);

Эта операция может возвращать либо нормальное сообщение, содержащее число float, либо сообщение об ошибке, содержащее код ошибки и ее описание. Для этого следует предусмотреть вызов исключения FaultException при реализации службы.

Можно указать дополнительные сообщения об ошибках с помощью атрибута FaultContractAttribute. Необходима возможность сериализации дополнительных ошибок с помощью кода DataContractSerializer, как показано в следующем примере.

[OperationContract]
[FaultContract(typeof(ItineraryNotAvailableFault))]
float GetAirfare(string fromCity, string toCity, DateTime date);

//code omitted

[DataContract]
public class ItineraryNotAvailableFault
{
    [DataMember]
    public bool IsAlternativeDateAvailable;

    [DataMember]
    public DateTime alternativeSuggestedDate;
}

Эти дополнительные ошибки можно создать вызовом исключения FaultException с соответствующим типом данных контракта. Дополнительные сведения см. в разделе Обработка исключений и сбоев.

Использовать класс XmlSerializer для описания ошибок не допускается. Атрибут XmlSerializerFormatAttribute не влияет на контракты ошибок.

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

Иногда имеет смысл использовать базовый тип в контракте операции или сообщения, а затем (при фактическом вызове операции) производный тип. В этом случае необходимо использовать либо атрибут ServiceKnownTypeAttribute, либо некий альтернативный механизм, позволяющий использовать производные типы. Рассмотрим следующую операцию.

[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);

Предположим, что два типа, Book и Magazine, унаследованы от LibraryItem. Чтобы использовать их в операции IsLibraryItemAvailable, можно изменить ее следующим образом:

[OperationContract]

[ServiceKnownType(typeof(Book))]

[ServiceKnownType(typeof(Magazine))]

public bool IsLibraryItemAvailable(LibraryItem item);

Или же можно воспользоваться атрибутом KnownTypeAttribute, если атрибут по умолчанию DataContractSerializer уже используется, как показано в следующем примере кода.

[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);

// code omitted 

[DataContract]
[KnownType(typeof(Book))]
[KnownType(typeof(Magazine))]
public class LibraryItem
{
    //code omitted
}

Для этого примените атрибут XmlIncludeAttribute при использовании XmlSerializer.

Атрибут ServiceKnownTypeAttribute можно применить к операции или ко всей службе. Он принимает либо тип, либо имя метода, при вызове которого возвращается список известных типов (подобно атрибуту KnownTypeAttribute). Дополнительные сведения см. в разделе Известные типы контрактов данных.

Указание назначения и стиля

Для описания служб с помощью языка WSDL наиболее часто используются следующие две службы: Document и RPC (удаленный вызов процедуры). В стиле Document тело сообщения полностью описывается с помощью схемы, а в языке WSDL различные части тела сообщения описываются ссылками на элементы в этой схеме. В стиле RPC код WSDL ссылается на тип схемы для каждой части сообщения, а не элемент. В некоторых случаях необходимо выбрать один из этих типов вручную. Для этого можно применить атрибут DataContractFormatAttribute и задать свойство Style (когда используется DataContractSerializer) или задать Style атрибуту XmlSerializerFormatAttribute (когда используется XmlSerializer).

Кроме того, XmlSerializer поддерживает две формы сериализованного XML: Literal и Encoded. Literal — наиболее распространенная форма, а также единственная форма, которую поддерживает DataContractSerializer. Encoded — форма из предыдущих версий, описанная в разделе 5 спецификации SOAP; использовать ее в новых службах не рекомендуется. Чтобы перейти в режим Encoded, задайте свойству Use атрибута XmlSerializerFormatAttribute значение Encoded.

В большинстве случаев лучше не изменять параметры по умолчанию для свойств Style и Use.

Управление процессом сериализации

Сериализацию данных можно настраивать различными способами.

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

Если используется сериализатор по умолчанию DataContractSerializer, можно управлять некоторыми аспектами процесса сериализации в службе путем применения к ней атрибута ServiceBehaviorAttribute. В частности, с помощью свойства MaxItemsInObjectGraph можно задать максимальную квоту на количество объектов, десериализуемых с помощью DataContractSerializer. С помощью свойства IgnoreExtensionDataObject можно отключить функцию управления версиями при круговом пути. Дополнительные сведения квотах см. в разделе Вопросы безопасности для данных. Дополнительные сведения круговом пути см. в разделе Контракты данных, совместимые с любыми будущими изменениями.

[ServiceContract]
[ServiceBehavior(MaxItemsInObjectGraph=100000)]
public interface IDataService
{
    [OperationContract] DataPoint[] GetData();
}

Поведения сериализации

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

Впрочем, у поведения DataContractSerializerOperationBehavior имеются свойства MaxItemsInObjectGraph, IgnoreExtensionDataObject и DataContractSurrogate, позволяющие настроить процесс сериализации. Назначение первых двух из вышеперечисленных свойств аналогично рассмотренному в предыдущем разделе. С помощью свойства DataContractSurrogate можно включить суррогаты контрактов данных — мощный механизм настройки и расширения процесса сериализации. Дополнительные сведения см. в разделе Суррогаты контрактов данных.

С помощью поведения DataContractSerializerOperationBehavior можно настроить сериализацию как для клиента, так и для сервера. В следующем примере показано, как увеличить квоту MaxItemsInObjectGraph для клиента.

ChannelFactory<IDataService> factory = new ChannelFactory<IDataService>(binding, address);
foreach (OperationDescription op in factory.Endpoint.Contract.Operations)
{
    DataContractSerializerOperationBehavior dataContractBehavior =
                op.Behaviors.Find<DataContractSerializerOperationBehavior>()
                as DataContractSerializerOperationBehavior;
    if (dataContractBehavior != null)
    {
        dataContractBehavior.MaxItemsInObjectGraph = 100000;
    }
}
IDataService client = factory.CreateChannel();

Ниже приведен эквивалентный код для службы (в данном случае — резидентной).

ServiceHost serviceHost = new ServiceHost(typeof(IDataService))
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.MaxItemsInObjectGraph = 100000;
        }
}
}
serviceHost.Open();

Если клиент размещен в Интернете, необходимо создать новый класс, производный от ServiceHost, и подключить его с помощью фабрики узла служб.

Управление параметрами сериализации в конфигурации

Параметры MaxItemsInObjectGraph и IgnoreExtensionDataObject можно изменять с помощью конфигурации, используя конечную точку dataContractSerializer или поведение службы, как показано в следующем примере.

<configuration>
    <system.serviceModel>
        <behaviors>
            <endpointBehaviors>
                <behavior name="LargeQuotaBehavior">
                    <dataContractSerializer
                      maxItemsInObjectGraph="100000" />
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <client>
            <endpoint address=http://example.com/myservice
                  behaviorConfiguration="LargeQuotaBehavior"
                binding="basicHttpBinding" bindingConfiguration="" 
                            contract="IDataService"
                name="" />
        </client>
    </system.serviceModel>
</configuration>

Сериализация общих типов, сохранение графов объектов и пользовательские сериализаторы

DataContractSerializer при сериализации использует имена контрактов данных, а не имена типов .NET. Это соответствует принципам сервисноориентированной архитектуры и повышает степень гибкости, поскольку типы .NET могут изменяться, не затрагивая при этом сетевой контракт. В редких случаях имеет смысл сериализовать имена типов .NET, обеспечив таким образом тесное соединение клиента и сервера (подобно технологии удаленного взаимодействия платформы .NET Framework). Поступать таким образом не рекомендуется, за исключением редких случаев, возникающих при миграции с технологий удаленного взаимодействия WCF на аналогичные технологии .NET Framework. В этом случае необходимо использовать класс NetDataContractSerializer вместо класса DataContractSerializer.

DataContractSerializer обычно сериализует графы объектов как деревья объектов. А именно, если один объект указан неоднократно, он сериализуется неоднократно. Рассмотрим, к примеру, экземпляр класса PurchaseOrder, имеющий два поля типа Address с именами billTo и shipTo. Если оба поля заданы одному и тому же экземпляру класса Address, после сериализации и десериализации получится два идентичных экземпляра этого класса. Это происходит из-за отсутствия стандартного (и поддерживающего взаимодействие) способа представления графов объектов в XML (за исключением стандарта предыдущих версий с кодировкой SOAP, используемого в сериализаторе XmlSerializer, как указано в предыдущем разделе о Style и Use). Сериализация графов объектов как деревьев имеет определенные преимущества: например, невозможность сериализации графов с циклическими ссылками. Иногда требуется переключиться на истинную сериализацию графов объектов, даже несмотря на потерю возможностей взаимодействия. Это достигается с помощью сериализатора DataContractSerializer, созданного с параметром preserveObjectReferences, имеющим значение true.

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

Во всех трех предыдущих случаях (сохранение типов .NET, сохранение графов объектов и сериализация на базе пользовательского сериализатора XmlObjectSerializer) требуется подключить пользовательский сериализатор. Для этого необходимы следующие действия.

  1. Самостоятельно составьте код поведения, наследуемого от класса DataContractSerializerOperationBehavior.

  2. Переопределите два метода CreateSerializer так, чтобы они возвращали ваш сериализатор (либо NetDataContractSerializer, т. е. сериализатор DataContractSerializer, у которого для preserveObjectReferences задано значение true, либо полностью созданный вами сериализатор XmlObjectSerializer).

  3. Прежде чем открывать узел службы или создавать канал клиента, удалите существующее поведение DataContractSerializerOperationBehavior и подключите производный пользовательский класс, созданный на предыдущих этапах.

Дополнительные сведения более сложных принципах сериализации см. в разделе Сериализация и десериализация.

См. также

Задачи

Как включать потоковую передачу
Как создать базовый контракт данных для класса или структуры

Основные понятия

Использование класса XmlSerializer