Serialization and deserialization are accomplished with the data contract surrogate when using DataContractSerializer to convert from .NET Framework to a suitable format, such as XML. Data contract surrogate can also be used to modify the metadata exported for types, when producing metadata representations such as XML Schema Documents (XSD). Upon import, code is created from metadata and the surrogate can be used in this case to customize the generated code as well.
To use the data contract surrogate, implement the IDataContractSurrogate interface.
The following is an overview of each method of IDataContractSurrogate with a possible implementation.
GetDataContractType
The GetDataContractType method maps one type to another. This method is required for serialization, deserialization, import, and export.
The first task is defining what types will be mapped to other types. For example:
-
On serialization, the mapping returned by this method is subsequently used to transform the original instance to a surrogated instance by calling the GetObjectToSerialize method.
-
On deserialization, the mapping returned by this method is used by the serializer to deserialize into an instance of the surrogate type. It subsequently calls GetDeserializedObject to transform the surrogated instance into an instance of the original type.
-
On export, the surrogate type returned by this method is reflected to get the data contract to use for generating metadata.
-
On import, the initial type is changed to a surrogate type that is reflected to get the data contract to use for purposes like referencing support.
The Type parameter is the type of the object that is being serialized, deserialized, imported, or exported. The GetDataContractType method must return the input type if the surrogate does not handle the type. Otherwise, return the appropriate surrogated type. If several surrogate types exist, numerous mappings can be defined in this method.
The GetDataContractType method is not called for built-in data contract primitives, such as Int32 or String. For other types, such as arrays, user-defined types, and other data structures, this method will be called for each type.
In the previous example, the method checks if the type parameter and Inventory are comparable. If so, the method maps it to InventorySurrogated. Whenever a serialization, deserialization, import schema, or export schema is called, this function is called first to determine the mapping between types.
GetObjectToSerialize Method
The GetObjectToSerialize method converts the original type instance to the surrogated type instance. The method is required for serialization.
The next step is to define the way the physical data will be mapped from the original instance to the surrogate by implementing the GetObjectToSerialize method. For example:
The GetObjectToSerialize method is called when an object is serialized. This method transfers data from the original type to the fields of the surrogated type. Fields can be directly mapped to surrogate fields, or manipulations of the original data may be stored in the surrogate. Some possible uses include: directly mapping the fields, performing operations on the data to be stored in the surrogated fields, or storing the XML of the original type in the surrogated field.
The targetType parameter refers to the declared type of the member. This parameter is the surrogated type returned by the GetDataContractType method. The serializer does not enforce that the object returned is assignable to this type. The obj parameter is the object to serialize, and will be converted to its surrogate if necessary. This method must return the input object if the surrogated does not handle the object. Otherwise, the new surrogate object will be returned. The surrogate is not called if the object is null. Numerous surrogate mappings for different instances may be defined within this method.
When creating a DataContractSerializer, you can instruct it to preserve object references. (For more information, see Serialization and Deserialization.) This is done by setting the preserveObjectReferences parameter in its constructor to true. In that case, the surrogate is called only once for an object since all subsequent serializations just write the reference into the stream. If preserveObjectReferences is set to false, then the surrogate is called every time an instance is encountered.
If the type of the instance serialized differs from the declared type, type information is written into the stream, for example, xsi:type to allow the instance to be deserialized at the other end. This process occurs whether the object is surrogated or not.
The example above converts the data of the Inventory instance to that of InventorySurrogated. It checks the type of the object and performs the necessary manipulations to convert to the surrogated type. In this case, the fields of the Inventory class are directly copied over to the InventorySurrogated class fields.
GetDeserializedObject Method
The GetDeserializedObject method converts the surrogated type instance to the original type instance. It is required for deserialization.
The next task is to define the way the physical data will be mapped from the surrogate instance to the original. For example:
This method is called only during the deserialization of an object. It provides reverse data mapping for the deserialization from the surrogate type back to its original type. Similar to the GetObjectToSerialize method, some possible uses may be to directly exchange field data, perform operations on the data, and store XML data. When deserializing, you may not always obtain the exact data values from original due to manipulations in the data conversion.
The targetType parameter refers to the declared type of the member. This parameter is the surrogated type returned by the GetDataContractType method. The obj parameter refers to the object that has been deserialized. The object can be converted back to its original type if it is surrogated. This method returns the input object if the surrogate does not handle the object. Otherwise, the deserialized object will be returned once its conversion has been completed. If several surrogate types exist, you may provide data conversion from surrogate to primary type for each by indicating each type and its conversion.
When returning an object, the internal object tables are updated with the object returned by this surrogate. Any subsequent references to an instance will obtain the surrogated instance from the object tables.
The previous example converts objects of type InventorySurrogated back to the initial type Inventory. In this case, data is directly transferred back from InventorySurrogated to its corresponding fields in Inventory. Because there are no data manipulations, the each of the member fields will contain the same values as before the serialization.
GetCustomDataToExport Method
When exporting a schema, the GetCustomDataToExport method is optional. It is used to insert additional data or hints into the exported schema. Additional data can be inserted at the member level or type level. For example:
This method (with two overloads) enables the inclusion of extra information into the metadata either at the member or type level. It is possible to include hints about whether a member is public or private, and comments which would be preserved throughout the export and import of the schema. Such information would be lost without this method. This method does not cause the insertion or deletion of members or types, but rather adds additional data to the schemas at either of these levels.
The method is overloaded and can take either a Type (clrtype parameter) or MemberInfo (memberInfo parameter). The second parameter is always a Type (dataContractType parameter). This method is called for every member and type of the surrogated dataContractType type.
Either of these overloads must return either null or a serializable object. A non-null object will be serialized as annotation into the exported schema. For the Type overload, each type that is exported to schema is sent to this method in the first parameter along with the surrogated type as the dataContractType parameter. For the MemberInfo overload, each member that is exported to schema sends its information as the memberInfo parameter with the surrogated type in the second parameter.
GetCustomDataToExport Method (Type, Type)
GetCustomDataToExport Method (MemberInfo, Type)
The System.Runtime.Serialization.IDataContractSurrogate.GetCustomDataToExport(System.Reflection.MemberInfo,System.Type) is called during export for every member in the types that are exported. This function enables you to customize any comments for the members that will be included in the schema upon export. The information for every member within the class is sent to this method to check whether any additional data need to be added in the schema.
The example above searches through the dataContractType for each member of the surrogate. It then returns the appropriate access modifier for each field. Without this customization, the default value for access modifiers is public. Therefore, all members would be defined as public in the code generated using the exported schema no matter what their actual access restrictions are. When not using this implementation, the member numpens would be public in the exported schema even though it was defined in the surrogate as private. Through the use of this method, in the exported schema, the access modifier can be generated as private.
GetReferencedTypeOnImport Method
This method maps the Type of the surrogate to the original type. This method is optional for schema importation.
When creating a surrogate that imports a schema and generates code for it, the next task is to define the type of a surrogate instance to its original type.
If the generated code needs to reference an existing user type, this is done by implementing the GetReferencedTypeOnImport method.
When importing a schema, this method is called for every type declaration to map the surrogated data contract to a type. The string parameters typeName and typeNamespace define the name and namespace of the surrogated type. The return value for GetReferencedTypeOnImport is used to determine whether a new type needs to be generated. This method must return either a valid type or null. For valid types, the type returned will be used as a referenced type in the generated code. If null is returned, no type will be referenced and a new type must be created. If several surrogates exist, it is possible to perform the mapping for each surrogate type back to its initial type.
The customData parameter is the object originally returned from GetCustomDataToExport. This customData is used when surrogate authors want to insert extra data/hints into the metadata to use during import to generate code.
ProcessImportedType Method
The ProcessImportedType method customizes any type created from schema importation. This method is optional.
When importing a schema, this method allows for any imported type and compilation information to be customized. For example:
During import, this method is called for every type generated. Change the specified CodeTypeDeclaration or modify the CodeCompileUnit. This includes changing the name, members, attributes, and many other properties of the CodeTypeDeclaration. By processing the CodeCompileUnit, it is possible to modify the directives, namespaces, referenced assemblies, and several other aspects.
The CodeTypeDeclaration parameter contains the code DOM type declaration. The CodeCompileUnit parameter allows for modification for processing the code. Returning null results in the type declaration being discarded. Conversely, when returning a CodeTypeDeclaration, the modifications are preserved.
If custom data is inserted during metadata export, it needs to be provided to the user during import so that it can be used. This custom data can be used for programming model hints, or other comments. Each CodeTypeDeclaration and CodeTypeMember instance includes custom data as the UserData property, cast to the IDataContractSurrogate type.
The example above performs some changes on the schema imported. The code preserves private members of the original type by using a surrogate. The default access modifier when importing a schema is public. Therefore, all members of the surrogate schema will be public unless modified, as in this example. During export, custom data is inserted into the metadata about which members are private. The example looks up the custom data, checks whether the access modifier is private, and then modifies the appropriate member to be private by setting its attributes. Without this customization, the numpens member would be defined as public instead of private.
GetKnownCustomDataTypes Method
This method obtains custom data types defined from the schema. The method is optional for schema importation.
The method is called at the beginning of schema export and import. The method returns the custom data types used in the schema exported or imported. The method is passed a Collection (the customDataTypes parameter), which is a collection of types. The method should add additional known types to this collection. The known custom data types are needed to enable serialization and deserialization of custom data using the DataContractSerializer. For more information, see Data Contract Known Types.