Controlling WSDL minOccurs with WCF

by Marcel Veldhuizen 8. August 2010 16:25

Last week, I got an e-mail from a customer using our web services. They were wondering some operations were not working properly when no input parameters were given. This raised an eyebrow at first, as my documentation clearly stated that this information was mandatory.

It would soon become clear, that the developer in question did not read said documentation at all, but was in fact flying blind on the WSDL. Indeed, the input message schema said everything was optional; this is default behavior for WCF. In this article, I'll work on fixing this minor caveat in the interest of interoperability.

Consider this contract definition:

[ServiceContract]
public interface ICalculatorService
{
    [OperationContract]
    int Add(int firstValue, int secondValue);
}

The operation takes two parameters. Obviously, neither of them is intended to be optional. However, when we look at the schema that is generated for the SOAP message, you'll notice something is off:

<xs:element name="Add">
    <xs:complexType>
        <xs:sequence>
            <xs:element minOccurs="0" name="firstValue" type="xs:int"/>
            <xs:element minOccurs="0" name="secondValue" type="xs:int"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

The minOccurs="0" attribute means that this particular value can be omitted (in which case the default value is used). Unfortunately, this does not communicate the intent of the service operation very well. Furthermore, some tools for generating client proxies — particularly on the Java platform — will use this information and generate more complex code than they otherwise would if these parameters were required to be present.

So, how do we get WCF to generate a schema with minOccurs="1"? Unfortunately, there is no easy way. As a result, some people suggest manually editing the WSDL and hosting is statically, but that is a maintenance nightmare waiting to happen.

Taking control of the schema

WCF is very flexible; it allows you to influence just about everything that's part of its default behavior. Metadata (WSDL) generation is no exception to this. It does get a little bit tricky however.

The solution I'm going to show here will make it so that any parameters must be specified, unless you explicitly make it optional through the use of a custom attribute. It makes most sense to do this at the service contract level, which is why the IContractBehavior interface comes into play. The key to changing the schema output itself, is the IWsdlExportExtension interface. Both of these are implemented by RequiredParametersBehaviorAttribute, which does all the dirty work (download).

The updated service contract:

[ServiceContract]
[RequiredParametersBehavior]
public interface ICalculatorService
{
    [OperationContract]
    int Add(int firstValue, int secondValue);
}

Seems simple enough, no? Just add an attribute... on to the actual hard work.

IWsdlExportExtension contains two methods: ExportContract and ExportEndpoint. At first glance, ExportContract sounds like the one we need, but it turns out we actually need both. ExportContract is called first when metadata is requested:

void IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext context);

We can manipulate the service metadata through the object passed in the context parameter. However, the used object model doesn't provide us with a way to influence the outcome of the minOccurs XSD attribute; we will have to manipulate the XSD more directly. At this point in time, no schemas have been generated yet however, and we don't want to generate them from scratch. By the time ExportEndpoint gets called, the XML schemas have been generated, ready to be manipulated by us.

We're getting ahead of ourselves though. How do we know what to change? The context passed to ExportContract can give us the information we need: a list of operations in the contract, and a list of parameters for each of them. We'll just store this information in a list for now:

// Code posted here is slightly simplified.
// See download for full code.
void IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
    foreach (var operation in context.Contract.Operations)
    {
        var inputMessage = operation.Messages.
            Where(m => m.Direction == MessageDirection.Input).First();
        var parameters = operation.SyncMethod.GetParameters();

        for (int i = 0; i < parameters.Length; i++)
        {
            object[] attributes = parameters[i].GetCustomAttributes(
                typeof(OptionalAttribute), false);

            if (attributes.Length == 0)
            {
                // The parameter has no [Optional] attribute, add it
                // to the list of parameters that we need to adjust
                // the XML schema for later on.
                _requiredParameter.Add(new RequiredMessagePart()
                    {
                        Namespace = inputMessage.Body.Parts[i].Namespace,
                        Message = operation.Name,
                        Name = inputMessage.Body.Parts[i].Name
                    });
            }
        }
    }
}

Remember the XSD we were trying to change? If not, it might help to take a quick glance at it again now, because the object model we use for the manipulation shows the same hierarchy. The input message consists of a complex type, which is a sequence of two simple "int" types. We find the schema generated for the namespace that the message is in, look for the appropriate input message definition, and change the desired element in the sequence:

// Code posted here is slightly simplified.
// See download for full code.
void IWsdlExportExtension.ExportEndpoint(WsdlExporter exporter, WsdlEndpointConversionContext context)
{
    foreach (var p in _requiredParameter)
    {
        var schemas = exporter.GeneratedXmlSchemas.Schemas(p.Namespace);

        foreach (XmlSchema schema in schemas)
        {
            var message = (XmlSchemaElement)schema.Elements[p.XmlQualifiedName];
            var complexType = (XmlSchemaComplexType)message.ElementSchemaType;
            var sequence = (XmlSchemaSequence)complexType.Particle;

            foreach (XmlSchemaElement item in sequence.Items)
            {
                if (item.Name == p.Name)
                {
                    item.MinOccurs = 1;
                    item.MinOccursString = "1";
                }
            }
        }
    }
}

That's it. Now, when you request your service metadata, it will look like this:

<xs:element name="Add">
    <xs:complexType>
        <xs:sequence>
            <xs:element minOccurs="1" name="firstValue" type="xs:int"/>
            <xs:element minOccurs="1" name="secondValue" type="xs:int"/>
        </xs:sequence>
    </xs:complexType>
</xs:element>

If you've been reading closely, you might remember I mentioned a second interface: IContractBehavior. We only need to implement it so that WCF will use our IWsdlExportExtension; all the method bodies are completely empty.

Optional parameters

Now, what if we want some parameter to actually be optional? This can be done with an attribute as I have briefly mentioned before. The attribute implementation is very simple:

[AttributeUsage(AttributeTargets.Parameter)]
public class OptionalAttribute : Attribute
{
}

This is what a service contract using this attribute might look like:

[ServiceContract]
[RequiredParametersBehavior]
public interface IGreetingService
{
    [OperationContract]
    string Greet(string name, [Optional] string language);
}

Instead of using an Optional attribute, you might choose to create a Required attribute instead. It should be simple enough to adapt the code for that.

Ideas for improvement

I'll readily admit that this is Blogware. I've made a number of assumptions in these code snippets, and I haven't tested this extensively in the field (yet). Edit 2011-04-14: one of my readers pointed out a problem with multiple endpoints that use the same contract. I've updated the downloadable source code accordingly. Thanks Martin!

If you run into trouble using this, please let me know. Some notes and possible improvements:

  • Just because your metadata now says the parameter is required, doesn't mean that WCF will actually enforce this. The goal here was to make the service more self-documenting, nothing else.
  • The example given here only processes input messages. You might want to modify output messages as well; it can be done in a very similar fashion.
  • Without modification, this example will not work if you do not use an interface to define your service contract. This should be easily fixed, but I strongly recommend that you use an interface for all your service contracts.
  • Rather than using a list to store the parameters that need changing, it might be cleaner to just store a reference to the WsdlContractConversionContext and do all the actual work in ExportEndpoint (edit: 2010-08-09)

Files:

Tags: , , , ,

Programming | .NET

Comments (9) -

Martin Pischky Germany
4/14/2011 8:54:27 AM #

Greate arcticle on fixing this annoying minOccures="0" that WCF generates. Has takes some time add support for return values, but now it works. Thanks, Martin

Reply

Sudeep India
8/11/2011 7:10:40 AM #

Nice article.... insightful indeed !!!

Reply

Gareth United Kingdom
8/23/2011 7:00:07 AM #

Very nifty, thanks.  

Is it possible to do something similar with DataMembers in complex types?
Is there a setting / attribute within the existing framework that I'm missing or do I have to write my own like you have for parameters?

Reply

Marcel Netherlands
8/23/2011 8:03:15 AM #

Hi Gareth,

This is built-in functionality for data contracts. You need to use the IsRequired parameter for the DataMemberAttribute:

[DataMember(IsRequired = true)]
public string MyProperty
{
    // ...
}

Reply

Mars Netherlands
9/8/2011 8:01:18 AM #

Thanks for this. One note though: it doesn't work correctly for out parameters, because they are part of the output message, but counted as input parameter. A quick fix for this is replacing this line in your example
var parameters = operation.SyncMethod.GetParameters();
with:
var parameters = operation.SyncMethod.GetParameters().Where( pi => !pi.IsOut ).ToArray();

Reply

yaron Israel
1/7/2012 10:56:46 AM #

Hi,
Great article..

Few questions:
1) Is there any implemnetation for adding regular expression (xs:restrictions) on some of the properties that being expose by the data contract ?
2) How can I debug this code?

Reply

jupeos United States
1/26/2012 12:11:43 PM #

Thanks Marcel.
Almost worked for me. I had to add a neutering of the nillable attribute too:

if (item.Name == p.Name)
{
    item.MinOccurs = 1;
    item.MinOccursString = "1";
    item.IsNillable = false;
    break;
}

Reply

aloe vera gel United States
9/13/2014 9:51:41 PM #

Excellent blog! Do you have any suggestions for aspiring writers? I'm hoping to start my own site soon but I'm a little lost on everything. Would you advise starting with a free platform like Wordpress or go for a paid option? There are so many options out there that I'm totally confused .. Any suggestions? Cheers!

Also visit my web-site;  aloe vera gel - http://cause57.hpage.com

Reply

kik android app United States
9/17/2014 4:18:29 AM #

It's not my first time to pay a quick visit this website, i am browsing this web site dailly and take good information from here all the time.

Reply

Pingbacks and trackbacks (1)+

Add comment

Marcel Veldhuizen

Software Architect by profession, mostly focused on Microsoft .NET technology.

Metal enthusiast; I love going to concerts :)

 

RecentPosts

Recent Tweets

Note: For Customization and Configuration, CheckOut Recent Tweets Documentation