This post has the goal to explain how it is possible to make Xsd annotations in the Wsdl generated by Windows Communication Foundation (the version we are using in our project is WinFx 3.0 Beta 2 Go-Live).
Unfortunately, Indigo does not allow to specify an annotation in the DataMember attribute. So, we can't do this,
[DataContract]
public class MyDataContract
{
[DataMember(XsdAnnotation="MyAnnotation")]
public string MyDataMember;
}
What we want is something like this,
[DataContract] public class MyDataContract
{
[DataMember]
[DataMemberAnnotation("MyAnnotation")]
public string MyDataMember;
}
and in the generated Wsdl we expect to have in our schema an Xsd annotation associated with the element MyDataMember.
First of all we must create our attribute,
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true,
AllowMultiple = false)]
public class DataMemberAnnotationAttribute : Attribute
{
private string m_Annotation;
public DataMemberAnnotationAttribute(string annotation)
{
m_Annotation = annotation;
}
public string Annotation
{
get
{
return m_Annotation;
}
}
}
Let me say that the solution we present in this post required some time using Lutz Roeder's .NET Reflector to understand the Wsdl generation process of the DataContractSerializer class. The class responsible for all this process is the DataContractSerializerOperationBehavior in the namespace System.ServiceModel.
It is very interesting to see that the internals of this process are already prepared to make Xsd annotations, however there is no simple way to do it. Maybe in the final version we have this feature simplified, who knows.
Basically, what we've to do is inherit from DataContractSerializerOperationBehavior and, in the new class created, tweak the wsdl generation. We're just intercepting the wsdl generation, so we've to implement the interface IWsdlExportExtension
public class DataContractWithXsdAnnotationsSerializerOperationBehavior:
DataContractSerializerOperationBehavior, IWsdlExportExtension
{
...
public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
}
public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
{
}
...
}
We've only to focus on this two methods to manipulate the wsdl generation. The simplest solution is to get from WsdlExporter the object responsible for the schema generation, which is an instance of the class XsdDataContractExporter, and set the ExportOptions property. One of the properties of the ExportOptions we can set is DataContractSurrogate, which have to implement the interface IDataContractSurrogate. This interface allow us to extend the generated schema. The IDataContractSurrogate is very simple, since we just want to change the export of schema.
public class DataContractXsdAnnotationSurrogate : IDataContractSurrogate
{
...
public object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType)
{
object[] dataMemberAnnotations =
memberInfo.GetCustomAttributes(typeof(DataMemberAnnotationAttribute), false);
if (dataMemberAnnotations != null && dataMemberAnnotations.Length > 0)
{
DataMemberAnnotationAttribute annotation =
dataMemberAnnotations[0] as DataMemberAnnotationAttribute;
return annotation.Annotation;
}
return null;
}
...
}
To get access to the XsdDataContractExporter, and change the IDataContractSurrogate to use, we implemented an helper method in our behavior
public class DataContractWithXsdAnnotationsSerializerOperationBehavior:
DataContractSerializerOperationBehavior, IWsdlExportExtension
{
...
void InitXsdDataContractExporter(WsdlExporter exporter)
{
object xsdDataContractExporterObj;
if (!exporter.State.TryGetValue(typeof(XsdDataContractExporter),
out xsdDataContractExporterObj))
{
xsdDataContractExporterObj =
new XsdDataContractExporter(exporter.GeneratedXmlSchemas);
exporter.State.Add(typeof(XsdDataContractExporter), xsdDataContractExporterObj);
}
XsdDataContractExporter xsdDataContractExporter =
xsdDataContractExporterObj as XsdDataContractExporter;
if (xsdDataContractExporter.Options == null)
{
xsdDataContractExporter.Options = new ExportOptions();
xsdDataContractExporter.Options.DataContractSurrogate =
new DataContractXsdAnnotationSurrogate();
}
}
...
}
Next, we just need to invoke this method before the export of the wsdl begins
public class DataContractWithXsdAnnotationsSerializerOperationBehavior:
DataContractSerializerOperationBehavior, IWsdlExportExtension
{
...
public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
InitXsdDataContractExporter(exporter);
...
}
public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
{
InitXsdDataContractExporter(exporter);
...
}
...
}
Now that we have the XsdDataContractExporter as we wish, with our custom IDataContractSurrogate, we can begin the wsdl generation. Unfortunately the methods ExportContract and ExportEndpoint, in our base class, are not public or protected so we can't do
base.ExportContract(exporter, context);
or
base.ExportEndpoint(exporter, context);
The less elegant part of the solution is the need to use reflection for the call,
typeof(IWsdlExportExtension).InvokeMember("ExportContract", BindingFlags.InvokeMethod, null, this, new object[] { exporter, context });
However, this does not work either, because we want to invoke the method in the base class and not in our instance. This complicates a little bit the solution because we need to have a reference to the original instance of the DataContractSerializerOperationBehavior. To do that, we must have another OperationBehavior which removes from the behaviors collection the DataContractSerializerOperationBehavior, and gives the reference of the instance that was removed to our DataContractWithXsdAnnotationsSerializerOperationBehavior, which must be added to the behaviors collection. Here it is,
public class ExportWsdlWithXsdAnnotationsOperationBehavior: Attribute, IOperationBehavior
{
...
public void ApplyDispatchBehavior(OperationDescription description,
System.ServiceModel.Dispatcher.DispatchOperation dispatch)
{
DataContractSerializerOperationBehavior dataContractSerializerOperationBehavior =
description.Behaviors.Find<DataContractSerializerOperationBehavior>();
//Remove the default operation behavior and add
//DataContractWithXsdAnnotationsSerializerOperationBehavior
if (dataContractSerializerOperationBehavior != null)
{
description.Behaviors.Remove(dataContractSerializerOperationBehavior);
description.Behaviors.Add(
new DataContractWithXsdAnnotationsSerializerOperationBehavior(
dataContractSerializerOperationBehavior, description,
dataContractSerializerOperationBehavior.DataContractFormatAttribute));
}
}
...
}
In DataContractWithXsdAnnotationsSerializerOperationBehavior we must receive the original
DataContractSerializerOperationBehavior in our constructors
public class DataContractWithAnnotationsSerializerOperationBehavior :
DataContractSerializerOperationBehavior, IWsdlExportExtension
{
private DataContractSerializerOperationBehavior m_DataContractSerializerOperationBehavior;
public DataContractWithXsdAnnotationsSerializerOperationBehavior(
DataContractSerializerOperationBehavior originalSerializer,
OperationDescription operation)
: base(operation)
{
m_DataContractSerializerOperationBehavior = originalSerializer;
}
public DataContractWithXsdAnnotationsSerializerOperationBehavior(
DataContractSerializerOperationBehavior originalSerializer,
OperationDescription operation,
DataContractFormatAttribute dataContractFormatAttribute)
: base(operation, dataContractFormatAttribute)
{
m_DataContractSerializerOperationBehavior = originalSerializer;
}
...
}
Finally, ExportContract and ExportEndpoint will look like this,
public class DataContractWithAnnotationsSerializerOperationBehavior :
DataContractSerializerOperationBehavior, IWsdlExportExtension
{
...
public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
InitXsdDataContractExporter(exporter);
typeof(IWsdlExportExtension).InvokeMember("ExportContract",
BindingFlags.InvokeMethod, null, m_DataContractSerializerOperationBehavior,
new object[] { exporter, context });
}
public void ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
{
InitXsdDataContractExporter(exporter);
typeof(IWsdlExportExtension).InvokeMember("ExportEndpoint",
BindingFlags.InvokeMethod, null, m_DataContractSerializerOperationBehavior,
new object[] { exporter, context });
}
...
}
And that's it. If we have this data contract
[DataContract]
public class GetAccountBalanceResponseMessage
{
[DataMember]
public string AccountNumber;
[DataMember]
[DataMemberAnnotation("This is the account balance")]
public decimal Balance;
}
We get this schema in wsdl
<xs:complexType name="GetAccountBalanceResponseMessage">
<xs:sequence>
<xs:element minOccurs="0" name="AccountNumber" nillable="true" type="xs:string" />
<xs:element minOccurs="0" name="Balance" type="xs:decimal">
<xs:annotation>
<xs:appinfo>
<Surrogate i:type="d1p1:string"
xmlns="http://schemas.microsoft.com/2003/10/Serialization/"
xmlns:d1p1="http://www.w3.org/2001/XMLSchema"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">This is the account balance</Surrogate>
</xs:appinfo>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
Here is the VS.NET solution to download.