Enumeration Classes, WCF and Service Metadata
October 25, 2012 Leave a comment
In a previous post I described the steps to hide enumeration classes from the consumers of a WCF service. That works fine if you manage your client’s contracts manually i.e. each client has its own service contract as well as client versions of all data contracts used in that service. This guarantees the cleanest separation between client and server. But it is not the most convenient way to consume a WCF service. Most projects I have seen use “Add Service Reference” instead to generate proxies from WSDL exposed by a ServiceMetadataEndpoint sometimes refered to as MEX.
And this is where the described steps fail. The metadata export will stil talk about the enumeration class. It will try to serialize the backing fields value
, name
and displayName
. That’s not what we want.
The neccessary information to fix this misbehavior is hidden at the very bottom of Microsoft’s help site about Data Contract Surrogates. The paragraph To Use a Surrogate for Metadata Export describes how you can hook up your custom surrogate with the infrastructure that is used to generate the WSDL for your service. If you include this code snippet in the behavior that decorates your service implementation your clients will only see the generated enum
class.
public class HideEnumerationClassesBehavior : Attribute, IServiceBehavior { private readonly IDataContractSurrogate surrogate; public HideEnumerationClassesBehavior() { this.surrogate = new EnumerationClassesSurrogate(); } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase host) { foreach (ServiceEndpoint endpoint in serviceDescription.Endpoints) { foreach (OperationDescription operation in endpoint.Contract.Operations) { DataContractSerializerOperationBehavior behavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); if (behavior == null) { behavior = new DataContractSerializerOperationBehavior(operation) { DataContractSurrogate = this.surrogate }; operation.Behaviors.Add(behavior); } else { behavior.DataContractSurrogate = this.surrogate; } } } this.HideEnumerationClassesFromMetadata(host); } private void HideEnumerationClassesFromMetadata(ServiceHostBase host) { ServiceMetadataBehavior smb = host.Description.Behaviors.Find<ServiceMetadataBehavior>(); if (smb == null) { return; } WsdlExporter exporter = smb.MetadataExporter as WsdlExporter; if (exporter == null) { return; } 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 = this.surrogate; } /* not implemented methods omitted for brevity */ }
We are getting there. One blemish that I almost missed is that the enum
Type parameters are exported as nullable. I guess this origins from the fact that the enumeration classes are reference and not value types. I needed to find a way around that issue as well.
This article by Carlos Figueira helped a lot.
I added an implementation of IWsdlExportExtension
to the above behavior as well as an empty implementation of IContractBehavior
. The later is neccessary to hook up the extension with the WCF infrastructure. After a lot of digging around in the contents of the System.Web.Services.Description namespace
I found the right hooks to replace method parameters of an enumeration class type with the corresponding enum type. The implementation of the IWsdlExportExtension.ExportContract
method now looks like this:
void IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext context) { foreach (var operation in context.Contract.Operations) { foreach (var message in operation.Messages) { foreach (var part in message.Body.Parts) { Type enumType; if (this.surrogate.Generator.TryGetEnumByType(part.Type, out enumType)) { part.Type = enumType; } } } } }
And finally we are there. We have completely hidden our server-side enumeration class from the service’s consumers. No matter if we use hand-crafted contracts or proxies generated from service metadata.
The complete source code is available here (project TecX.EnumClasses and the test suite that puts them to use can be found in TecX.EnumClasses.Test). There is a separate solution file TecX.EnumClasses as well. This solution contains additional projects that demonstrate client and server side use.