แก้ไข

แชร์ผ่าน


Using the XmlSerializer Class

Windows Communication Foundation (WCF) can use two different serialization technologies to turn the data in your application into XML that's transmitted between clients and services: DataContractSerializer and XmlSerializer.

DataContractSerializer

By default WCF uses the DataContractSerializer class to serialize data types. This serializer supports the following types:

  • Primitive types (for example, integers, strings, and byte arrays), as well as some special types, such as XmlElement and DateTime, which are treated as primitives.

  • Data contract types (types marked with the DataContractAttribute attribute).

  • Types marked with the SerializableAttribute attribute, which include types that implement the ISerializable interface.

  • Types that implement the IXmlSerializable interface.

  • Many common collection types, which include many generic collection types.

Many .NET Framework types fall into the latter two categories and are thus serializable. Arrays of serializable types are also serializable. For a complete list, see Specifying Data Transfer in Service Contracts.

The DataContractSerializer, used together with data contract types, is the recommended way to write new WCF services. For more information, see Using Data Contracts.

XmlSerializer

WCF also supports the XmlSerializer class. The XmlSerializer class is not unique to WCF. It is the same serialization engine that ASP.NET Web services use. The XmlSerializer class supports a much narrower set of types than the DataContractSerializer class, but allows much more control over the resulting XML and supports much more of the XML Schema definition language (XSD) standard. It also does not require any declarative attributes on the serializable types. For more information, see the XML Serialization topic in the .NET Framework documentation. The XmlSerializer class does not support data contract types.

When using Svcutil.exe or the Add Service Reference feature in Visual Studio to generate client code for a third-party service, or to access a third-party schema, an appropriate serializer is automatically selected for you. If the schema is not compatible with the DataContractSerializer, the XmlSerializer is selected.

Switch to the XmlSerializer

At times, you may have to manually switch to the XmlSerializer. This happens, for example, in the following cases:

  • When migrating an application from ASP.NET Web services to WCF, you may want to reuse existing, XmlSerializer-compatible types instead of creating new data contract types.

  • When precise control over the XML that appears in messages is important, but a Web Services Description Language (WSDL) document is not available, for example, when creating a service with types that have to comply to a certain standardized, published schema that is not compatible with the DataContractSerializer.

  • When creating services that follow the legacy SOAP Encoding standard.

In these and other cases, you can manually switch to the XmlSerializer class by applying the XmlSerializerFormatAttribute attribute to your service, as shown in the following code.

[ServiceContract]
[XmlSerializerFormat]
public class BankingService
{
[OperationContract]
    public void ProcessTransaction(BankingTransaction bt)
    {
        // Code not shown.
    }
}

//BankingTransaction is not a data contract class,
//but is an XmlSerializer-compatible class instead.
public class BankingTransaction
{
    [XmlAttribute]
    public string Operation;
    [XmlElement]
    public Account fromAccount;
    [XmlElement]
    public Account toAccount;
    [XmlElement]
    public int amount;
}
//Notice that the Account class must also be XmlSerializer-compatible.
<ServiceContract(), XmlSerializerFormat()> _
Public Class BankingService
    <OperationContract()> _
    Public Sub ProcessTransaction(ByVal bt As BankingTransaction)
        ' Code not shown.
    End Sub
End Class


' BankingTransaction is not a data contract class,
' but is an XmlSerializer-compatible class instead.

Public Class BankingTransaction
    <XmlAttribute()> _
    Public Operation As String
    <XmlElement()> _
    Public fromAccount As Account
    <XmlElement()> _
    Public toAccount As Account
    <XmlElement()> _
    Public amount As Integer
End Class
'Notice that the Account class must also be XmlSerializer-compatible.

Security Considerations

Note

It is important to be careful when switching serialization engines. The same type can serialize to XML differently depending on the serializer being used. If you accidentally use the wrong serializer, you might be disclosing information from the type that you did not intend to disclose.

For example, the DataContractSerializer class only serializes members marked with the DataMemberAttribute attribute when serializing data contract types. The XmlSerializer class serializes any public member. See the type in the following code.

[DataContract]
public class Customer
{
    [DataMember]
    public string firstName;
    [DataMember]
    public string lastName;
    public string creditCardNumber;
}
<DataContract()> _
Public Class Customer
    <DataMember()> _
    Public firstName As String
    <DataMember()> _
    Public lastName As String
    Public creditCardNumber As String
End Class

If the type is inadvertently used in a service contract where the XmlSerializer class is selected, the creditCardNumber member is serialized, which is probably not intended.

Even though the DataContractSerializer class is the default, you can explicitly select it for your service (although doing this should never be required) by applying the DataContractFormatAttribute attribute to the service contract type.

The serializer used for the service is an integral part of the contract and cannot be changed by selecting a different binding or by changing other configuration settings.

Other important security considerations apply to the XmlSerializer class. First, it is strongly recommended that any WCF application that uses the XmlSerializer class is signed with a key that is safeguarded from disclosure. This recommendation applies both when a manual switch to the XmlSerializer is performed and when an automatic switch is performed (by Svcutil.exe, Add Service Reference, or a similar tool). This is because the XmlSerializer serialization engine supports the loading of pre-generated serialization assemblies as long as they are signed with the same key as the application. An unsigned application is completely unprotected from the possibility of a malicious assembly matching the expected name of the pre-generated serialization assembly being placed in the application folder or the global assembly cache. Of course, an attacker must first gain write access to one of these two locations to attempt this action.

Another threat that exists whenever you use XmlSerializer is related to write access to the system temporary folder. The XmlSerializer serialization engine creates and uses temporary serialization assemblies in this folder. You should be aware that any process with write access to the temporary folder may overwrite these serialization assemblies with malicious code.

Rules for XmlSerializer support

You cannot directly apply XmlSerializer-compatible attributes to contract operation parameters or return values. However, they can be applied to typed messages (message contract body parts), as shown in the following code.

[ServiceContract]
[XmlSerializerFormat]
public class BankingService
{
    [OperationContract]
    public void ProcessTransaction(BankingTransaction bt)
    {
        //Code not shown.
    }
}

[MessageContract]
public class BankingTransaction
{
    [MessageHeader]
    public string Operation;
    [XmlElement, MessageBodyMember]
    public Account fromAccount;
    [XmlElement, MessageBodyMember]
    public Account toAccount;
    [XmlAttribute, MessageBodyMember]
    public int amount;
}
<ServiceContract(), XmlSerializerFormat()> _
Public Class BankingService
    <OperationContract()> _
    Public Sub ProcessTransaction(ByVal bt As BankingTransaction)
        'Code not shown.
    End Sub
End Class

<MessageContract()> _
Public Class BankingTransaction
    <MessageHeader()> _
    Public Operation As String
    <XmlElement(), MessageBodyMember()> _
    Public fromAccount As Account
    <XmlElement(), MessageBodyMember()> _
    Public toAccount As Account
    <XmlAttribute(), MessageBodyMember()> _
    Public amount As Integer
End Class

When applied to typed message members, these attributes override properties that conflict on the typed message attributes. For example, in the following code, ElementName overrides Name.

    [MessageContract]
    public class BankingTransaction
    {
        [MessageHeader] public string Operation;

        //This element will be <fromAcct> and not <from>:
        [XmlElement(ElementName="fromAcct"), MessageBodyMember(Name="from")]
        public Account fromAccount;

        [XmlElement, MessageBodyMember]
        public Account toAccount;

        [XmlAttribute, MessageBodyMember]
        public int amount;
}
<MessageContract()> _
Public Class BankingTransaction
    <MessageHeader()> _
    Public Operation As String

    'This element will be <fromAcct> and not <from>:
    <XmlElement(ElementName:="fromAcct"), _
        MessageBodyMember(Name:="from")> _
    Public fromAccount As Account

    <XmlElement(), MessageBodyMember()> _
    Public toAccount As Account

    <XmlAttribute(), MessageBodyMember()> _
    Public amount As Integer
End Class

The MessageHeaderArrayAttribute attribute is not supported when using the XmlSerializer.

Note

In this case, the XmlSerializer throws the following exception, which is released prior to WCF: "An element declared at the top level of a schema cannot have maxOccurs > 1. Provide a wrapper element for 'more' by using XmlArray or XmlArrayItem instead of XmlElementAttribute, or by using the Wrapped parameter style."

If you receive such an exception, investigate whether this situation applies.

WCF does not support the SoapIncludeAttribute and XmlIncludeAttribute attributes in message contracts and operation contracts; use the KnownTypeAttribute attribute instead.

Types that Implement the IXmlSerializable Interface

Types that implement the IXmlSerializable interface are fully supported by the DataContractSerializer. The XmlSchemaProviderAttribute attribute should always be applied to these types to control their schema.

Warning

If you are serializing polymorphic types you must apply the XmlSchemaProviderAttribute to the type to ensure the correct type is serialized.

There are three varieties of types that implement IXmlSerializable: types that represent arbitrary content, types that represent a single element, and legacy DataSet types.

  • Content types use a schema provider method specified by the XmlSchemaProviderAttribute attribute. The method does not return null and the IsAny property on the attribute is left at its default value of false. This is the most common usage of IXmlSerializable types.

  • Element types are used when an IXmlSerializable type must control its own root element name. To mark a type as an element type, either set the IsAny property on the XmlSchemaProviderAttribute attribute to true or return null from the schema provider method. Having a schema provider method is optional for element types – you may specify null instead of the method name in the XmlSchemaProviderAttribute. However, if IsAny is true and a schema provider method is specified, the method must return null.

  • Legacy DataSet types are IXmlSerializable types that are not marked with the XmlSchemaProviderAttribute attribute. Instead, they rely on the GetSchema method for schema generation. This pattern is used for the DataSet type and its typed dataset derives a class in earlier versions of the .NET Framework, but is now obsolete and is supported only for legacy reasons. Do not rely on this pattern and always apply the XmlSchemaProviderAttribute to your IXmlSerializable types.

IXmlSerializable Content Types

When serializing a data member of a type that implements IXmlSerializable and is a content type as defined previously, the serializer writes the wrapper element for the data member and passes control to the WriteXml method. The WriteXml implementation can write any XML, which includes adding attributes to the wrapper element. After WriteXml is done, the serializer closes the element.

When deserializing a data member of a type that implements IXmlSerializable and is a content type as defined previously, the deserializer positions the XML reader on the wrapper element for the data member and passes control to the ReadXml method. The method must read the entire element, including the start and end tags. Make sure your ReadXml code handles the case where the element is empty. Additionally, your ReadXml implementation should not rely on the wrapper element being named a particular way. The name is chosen by the serializer can vary.

It is permitted to assign IXmlSerializable content types polymorphically, for example, to data members of type Object. It is also permitted for the type instances to be null. Finally, it is possible to use IXmlSerializable types with object graph preservation enabled and with the NetDataContractSerializer. All these features require the WCF serializer to attach certain attributes into the wrapper element ("nil" and "type" in the XML Schema Instance namespace and "Id", "Ref", "Type" and "Assembly" in a WCF-specific namespace).

Attributes to Ignore when Implementing ReadXml

Before passing control to your ReadXml code, the deserializer examines the XML element, detects these special XML attributes, and acts on them. For example, if "nil" is true, a null value is deserialized and ReadXml is not called. If polymorphism is detected, the contents of the element are deserialized as if it was a different type. The polymorphically-assigned type’s implementation of ReadXml is called. In any case, a ReadXml implementation should ignore these special attributes because they are handled by the deserializer.

Schema Considerations for IXmlSerializable Content Types

When exporting schema and an IXmlSerializable content type, the schema provider method is called. An XmlSchemaSet is passed to the schema provider method. The method can add any valid schema to the schema set. The schema set contains the schema that is already known at the time when schema export occurs. When the schema provider method must add an item to the schema set, it must determine whether an XmlSchema with the appropriate namespace already exists in the set. If it does, the schema provider method must add the new item to the existing XmlSchema. Otherwise, it must create a new XmlSchema instance. This is important if arrays of IXmlSerializable types are being used. For example, if you have an IXmlSerializable type that gets exported as type "A" in namespace "B", it is possible that by the time the schema provider method is called the schema set already contains the schema for "B" to hold the "ArrayOfA" type.

In addition to adding types to the XmlSchemaSet, the schema provider method for content types must return a non-null value. It can return an XmlQualifiedName that specifies the name of the schema type to use for the given IXmlSerializable type. This qualified name also serves as the data contract name and namespace for the type. It is permitted to return a type that does not exist in the schema set immediately when the schema provider method returns. However, it is expected that by the time all related types are exported (the Export method is called for all relevant types on the XsdDataContractExporter and the Schemas property is accessed), the type exists in the schema set. Accessing the Schemas property before all relevant Export calls have been made can result in an XmlSchemaException. For more information about the export process, see Exporting Schemas from Classes.

The schema provider method can also return the XmlSchemaType to use. The type may or may not be anonymous. If it is anonymous, the schema for the IXmlSerializable type is exported as an anonymous type every time the IXmlSerializable type is used as a data member. The IXmlSerializable type still has a data contract name and namespace. (This is determined as described in Data Contract Names except that the DataContractAttribute attribute cannot be used to customize the name.) If it is not anonymous, it must be one of the types in the XmlSchemaSet. This case is equivalent to returning the XmlQualifiedName of the type.

Additionally, a global element declaration is exported for the type. If the type does not have the XmlRootAttribute attribute applied to it, the element has the same name and namespace as the data contract, and its "nillable" property is true. The only exception to this is the schema namespace (http://www.w3.org/2001/XMLSchema) – if the type’s data contract is in this namespace, the corresponding global element is in the blank namespace because it is forbidden to add new elements to the schema namespace. If the type has the XmlRootAttribute attribute applied to it, the global element declaration is exported using the following: ElementName, Namespace and IsNullable properties. The defaults with XmlRootAttribute applied are the data contract name, a blank namespace and "nillable" being true.

The same global element declaration rules apply to legacy dataset types. Note that the XmlRootAttribute cannot override global element declarations added through custom code, either added to the XmlSchemaSet using the schema provider method or through GetSchema for legacy dataset types.

IXmlSerializable Element Types

IXmlSerializable element types have either the IsAny property set to true or have their schema provider method return null.

Serializing and deserializing an element type is very similar to serializing and deserializing a content type. However, there are some important differences:

  • The WriteXml implementation is expected to write exactly one element (which could of course contain multiple child elements). It should not be writing attributes outside of this single element, multiple sibling elements or mixed content. The element may be empty.

  • The ReadXml implementation should not read the wrapper element. It is expected to read the one element that WriteXml produces.

  • When serializing an element type regularly (for example, as a data member in a data contract), the serializer outputs a wrapper element before calling WriteXml, as with content types. However, when serializing an element type at the top level, the serializer does not normally output a wrapper element at all around the element that WriteXml writes, unless a root name and namespace are explicitly specified when constructing the serializer in the DataContractSerializer or NetDataContractSerializer constructors. For more information, see Serialization and Deserialization.

  • When serializing an element type at the top level without specifying the root name and namespace at construction time, WriteStartObject and WriteEndObject essentially do nothing and WriteObjectContent calls WriteXml. In this mode, the object being serialized cannot be null and cannot be polymorphically assigned. Also, object graph preservation cannot enabled and the NetDataContractSerializer cannot be used.

  • When deserializing an element type at the top level without specifying the root name and namespace at construction time, IsStartObject returns true if it can find the start of any element. ReadObject with the verifyObjectName parameter set to true behaves in the same way as IsStartObject before actually reading the object. ReadObject then passes control to ReadXml method.

The schema exported for element types is the same as for the XmlElement type as described in an earlier section, except that the schema provider method can add any additional schema to the XmlSchemaSet as with content types. Using the XmlRootAttribute attribute with element types is not allowed, and global element declarations are never emitted for these types.

Differences from the XmlSerializer

The IXmlSerializable interface and the XmlSchemaProviderAttribute and XmlRootAttribute attributes are also understood by the XmlSerializer . However, there are some differences in how these are treated in the data contract model. The important differences are summarized in the following list:

  • The schema provider method must be public to be used in the XmlSerializer, but does not have to be public to be used in the data contract model.

  • The schema provider method is called when IsAny is true in the data contract model but not with the XmlSerializer.

  • When the XmlRootAttribute attribute is not present for content or legacy dataset types, the XmlSerializer exports a global element declaration in the blank namespace. In the data contract model, the namespace used is normally the data contract namespace as described earlier.

Be aware of these differences when creating types that are used with both serialization technologies.

Importing IXmlSerializable Schema

When importing a schema generated from IXmlSerializable types, there are a few possibilities:

  • The generated schema may be a valid data contract schema as described in Data Contract Schema Reference. In this case, schema can be imported as usual and regular data contract types are generated.

  • The generated schema may not be a valid data contract schema. For example, your schema provider method may generate schema that involves XML attributes that are not supported in the data contract model. In this case, you can import the schema as IXmlSerializable types. This import mode is not on by default but can easily be enabled – for example, with the /importXmlTypes command-line switch to the ServiceModel Metadata Utility Tool (Svcutil.exe). This is described in detail in the Importing Schema to Generate Classes. Note that you must work directly with the XML for your type instances. You may also consider using a different serialization technology that supports a wider range of schema – see the topic on using the XmlSerializer.

  • You may want to reuse your existing IXmlSerializable types in the proxy instead of generating new ones. In this case, the referenced types feature described in the Importing Schema to Generate Types topic can be used to indicate the type to reuse. This corresponds to using the /reference switch on svcutil.exe, which specifies the assembly that contains the types to reuse.

XmlSerializer Legacy Behavior

In the .NET Framework 4.0 and earlier, the XmlSerializer generated temporary serialization assemblies by writing C# code to a file. The file was then compiled into an assembly. This behavior had some undesirable consequences like slowing the startup time for the serializer. In .NET Framework 4.5, this behavior was changed to generate the assemblies without requiring use of the compiler. Some developers may wish to see the generated C# code. You can specify to use this legacy behavior by the following configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.xml.serialization>
    <xmlSerializer tempFilesLocation='e:\temp\XmlSerializerBug' useLegacySerializerGeneration="true" />
  </system.xml.serialization>
  <system.diagnostics>
    <switches>
      <add name="XmlSerialization.Compilation" value="1" />
    </switches>
  </system.diagnostics>
</configuration>

If you run into compatibility issues, such as the XmlSerializer failing to serialize a derived class with a non-public new override, you can switch back to the XMLSerializer legacy behavior by using the following configuration:

<configuration>
  <appSettings>
    <add key="System:Xml:Serialization:UseLegacySerializerGeneration" value="true" />
  </appSettings>
</configuration>

As an alternative to the above configuration, you can use the following configuration on a machine running .NET Framework 4.5 or later version:

<configuration>
  <system.xml.serialization>
    <xmlSerializer useLegacySerializerGeneration="true"/>
  </system.xml.serialization>
</configuration>

Note

The <xmlSerializer useLegacySerializerGeneration="true"/> switch only works on a machine running .NET Framework 4.5 or later version. The above appSettings approach works on all .NET Framework versions.

See also