Web Services

Extend the ASP.NET WebMethod Framework with Business Rules Validation

Aaron Skonnard and Dan Sullivan

Code download available at:BusinessRules.exe(223 KB)

This article assumes you're familiar with XML Schemas and Web Methods

Level of Difficulty123

SUMMARY

In an earlier article the authors showed how to build a custom WebMethods extension that provides XML Schema validation, a function that is lacking in ASP.NET. In the process they established a foundation for enforcing business rules during the deserialization of XML data. The technique, which is described in this article, uses declarative XPath assertions to test business rule compliance.

In building this business rules validation engine, the authors integrate the validation descriptions into the WSDL file that is automatically generated by the WebMethod infrastructure. Finally, they demonstrate how to extend wsdl.exe, the tool that generates WebMethod proxy/server code from WSDL files, to make use of their extensions.

Contents

Business Rules and XPath
Extending ValidationExtension
AssertAttribute
Extending GetInitializer
Extending ProcessMessage
Documenting Business Rules
WSDL Extensibility Elements
WSDL Extensibility Points
WSDL Generation, SoapExtensionReflector
Consuming WSDL
SoapExtensionImporter
Client-side SoapExtension Processing
Conclusion

Last month we showed you how to compensate for the lack of validation in ASP.NET WebMethods by using the extensibility hooks built into ASP.NET (see Web Services: Extend the ASP.NET WebMethod Framework by Adding XML Schema Validation). We showed how to build a custom extension that provides XML Schema validation (ValidationExtension) and gave you a sneak peek at another technique for enforcing business rules during deserialization. In this article, we'll continue that discussion by explaining how to implement an XPath-based business rules engine.

We'll also show you how to integrate the necessary validation descriptions into the Web Services Description Language (WSDL) file that is automatically generated by the WebMethod infrastructure. To complete the topic, we'll show you how to extend wsdl.exe, the tool that generates WebMethod proxy/server code from WSDL files, to include our extensions. The full source code is available for download from the link at the top of this article.

Business Rules and XPath

Business rules typically evolve from corporate specifications that describe actual business constraints. Developers who implement business rules often put comments in their code describing the rule and sometimes even reference the specification. As you might expect, this approach is a nightmare to manage since specifications, code, and comments will be harder to keep in sync. It would be much cleaner to declaratively include business rules in your class definitions, relying on some other piece of infrastructure to validate constraints and generate the documentation.

As we illustrated with the ValidationExtension example in the July 2003 issue, XML Schema validation may give you what you need to enforce certain information constraints. However, XML Schema cannot describe co-occurrence constraints, which are typical of most business rules in the real world, so it's not sufficient for building a generic business rules processing engine.

XPath, on the other hand, is an ideal language for defining additional message constraints. An XPath constraint is simply an expression that acts as an assertion: it must evaluate to true (similar to the type of expression you use in an XPath predicate). For example, the following XPath expression asserts that the size of the length element is greater than the size of the width element in a SOAP request message:

/s:Envelope/s:Body/t:CalcArea/t:length >= /s:Envelope/s:Body/t:CalcArea/t:width

If you're already using XML Schema to validate the message structure (or you just don't care where the elements exist in the document), you could use this abbreviated expression instead:

//t:length >= //t:width

Likewise, this expression ensures that the area that is calculated here is greater than 100 in all cases:

(//t:length * //t:width) > 100

The following expression verifies that if an optional username element exists, so does the password element:

not(//t:username) or (//t:username and //t:password)

Finally, this expression validates that a price element exists and depending on its value, there may also be a tax element:

//t:price[. < 100][not(following::t:tax)] or //t:price[. >=100][following::t:tax]

According to this expression, if the price is less than 100, there must not be a tax element, but if it's greater then 100, the tax element is required. These are all examples of constraints that cannot be described using XML Schema but are quite easily described using XPath expressions.

Extending ValidationExtension

We decided that it would be useful to build this type of XPath constraint processing right into the ValidationExtension class. To do so we defined a new attribute class called AssertAttribute that allows you to specify XPath assertions to either an individual method or the entire class (applying to all WebMethods). You can use any XPath expression in the declaration—it will be evaluated as a Boolean.

To evaluate the XPath expressions, the processor needs to know what's actually meant by the prefixes they use. This information is supplied to the processor through an XmlNamespaceManager object. Hence, we also defined an AssertNamespaceBindingAttribute class so users can specify the namespace prefix bindings required by the expressions; this attribute may only be used on the class definition. The code in Figure 1 contains a complete example using these attributes.

Figure 1 Using XPath Assertions in WebMethods

[WebService(Namespace="https://example.org/geometry/")] [AssertNamespaceBinding("s", "https://schemas.xmlsoap.org/soap/envelope/")] [AssertNamespaceBinding("t", "https://example.org/geometry/")] [Assert(@"//t:width >= 0", "width must be greater than 0")] [Assert("(//t:length > //t:width)", "Length must be greater than width")] public class Geometry { [WebMethod] [Validation] [Assert("(//t:length * //t:width) > 100", "Area must be greater than 100")] [Assert("(//t:length div //t:width) = 2", "Length must be exactly twice the width")] public double CalcArea(double length, double width) { return length * width; } [WebMethod] [Validation] public double CalcPerimeter(double length, double width) { return length *2 + width *2; } }

In this example, the class has two asserts that apply to both of the WebMethods: width >= 0 and length >= width. There are two additional asserts defined specifically for CalcArea: the area must be greater than 100 and the length must be twice the width.

We had to modify our GetInitializer implementation to save the compiled XPath expressions and namespace bindings for use in ProcessMessage. In addition, we also had to modify ProcessMessage in order to evaluate the expressions after the schema validation process completes.

Because we wanted to make it possible to handle XPath assertions without requiring XSD validation, we added a property to ValidationAttribute called SchemaValidation, which you can use to disable XSD validation:

[Validation(SchemaValidation=false)]

Now let's look at the new attributes and the changes to GetInitializer and ProcessMessage in more detail.

AssertAttribute

To help you understand how the various pieces fit together, let's first look at the AssertAttribute and AssertNamespaceBindingAttribute classes. AssertAttribute enables the user to provide the rule (such as the XPath expression) along with a human-readable description. It also provides an internal slot for storing the compiled XPath expression for future use in ProcessMessage. Figure 2 shows the complete class definition.

Figure 2 AssertAttribute Definition

// holds the compiled XPath expression & description [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class AssertAttribute : System.Attribute { string _rule; string _description; XPathExpression _expression; public AssertAttribute() : this("", "") { } public AssertAttribute(string rule) : this(rule, "") {} public AssertAttribute(string rule, string description) { _rule = rule; _description = description; _expression = null; } [XmlElement("expression")] public string Rule { get { return _rule; } set { _rule = value; } } [XmlElement("description")] public string Description { get { return _description; } set { _description = value; } } internal XPathExpression Expression { get { return _expression; } set { _expression = value; } } }

Notice that the attribute may be used on class or method declarations. When it's used on a method, the rule only applies to that method. When it's used on a class, it applies to all methods in the class. You may have also noticed the XmlElement declarations on the Rule and Description properties. These are used to control how the class is mapped to an XML message when it's serialized. Later we'll use XmlSerializer to report the attributes that failed to validate during the generation of the SoapException.

The AssertNamespaceBindingAttribute is much simpler since it just stores the prefix-to-namespace name bindings. See Figure 3 for the class definition. With these attributes in place, devs can provide all the necessary information for processing their business rules: the expressions, descriptions, and namespace bindings (see Figure 1 again). Now let's look at how to harvest this information in GetInitializer and store it for future use in ProcessMessage.

Figure 3 AssertNamespaceBindingAttribute Definition

// Specifies namespace bindings required by the XPath expressions [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class AssertNamespaceBindingAttribute : System.Attribute { string _prefix; string _ns; public AssertNamespaceBindingAttribute(string prefix, string ns) { _prefix = prefix; _ns = ns; } public string Prefix { get { return _prefix; } } public string Namespace { get { return _ns; } } }

Extending GetInitializer

The first thing we must do to extend our GetIntializer method is to modify the return type. When we were just performing XML Schema validation, we simply returned an XmlSchemaCollection object, but now that we've added XPath assertions we have more information to store. We defined a new class called ValidationContext for holding all of the initialization state that we'll need in ProcessMessage:

internal class ValidationContext { internal XmlNamespaceManager NamespaceManager = null; internal AssertAttribute[] AssertAttributes = null; internal XPathExpression CompleteRuleExpression = null; internal ValidationAttribute ValidationAttribute = null; internal XmlSchemaCollection SchemaCollection = null; }

It still contains an XmlSchemaCollection member, but this class also includes several other members. If the user configured the extension using a ValidationAttribute, we'll save it in the ValidationAttribute field. The other three fields are used for XPath processing: NamespaceManager holds an XmlNamespaceManager object initialized with all of the user-supplied AssertNamespaceBinding attributes, AssertAttributes contains the array of user-supplied Assert attributes, and CompleteRuleExpression contains the cumulative XPath expression generated from all of the individual expressions (this can be used for a quick success/failure test).

We've already discussed how the schema collection is loaded within GetInitializer (see our July 2003 article), but the others require some attention. At the beginning of GetInitializer, we instantiate a ValidationContext object in order to hold everything we're going to need later:

ValidationContext ctx = new ValidationContext();

Then we harvest the AssertNamespaceBinding attributes and save the supplied information into an XmlNamespaceManager object, which is then placed into the ValidationContext object, as shown in the code in Figure 4.

Figure 4 Storing AssertNamespaceBinding Attributes

// temporary navigator for compiling XPath expressions XmlDocument doc = new XmlDocument(); XPathNavigator nav = doc.CreateNavigator(); // namespace manager for holding all namespace bindings XmlNamespaceManager ns = new XmlNamespaceManager(nav.NameTable); // retrieve (user-provided) namespace binding attributes object[] namespaceAtts = serviceType.GetCustomAttributes( typeof(AssertNamespaceBindingAttribute), true); foreach(AssertNamespaceBindingAttribute nsa in namespaceAtts) ns.AddNamespace(nsa.Prefix, nsa.Namespace); // store namespace manager in context for future use ctx.NamespaceManager = ns;

Next, we harvest the Assert attributes from both the class and method (if it was configured on the method through an attribute). If both class and method attributes exist, we create a new AssertAttribute array and copy the objects into it. This simplifies the work we have to do in ProcessMessage later on (see Figure 5).

Figure 5 Harvesting the Assert Attributes

// retrieve (user-provided) assertion attributes AssertAttribute[] allRuleAtts = null; object[] classRuleAtts = serviceType.GetCustomAttributes( typeof(AssertAttribute), true); if (methodInfo != null) // configured via attribute { // retrieve method object[] methodRuleAtts = methodInfo.GetCustomAttributes( typeof(AssertAttribute)); allRuleAtts = new AssertAttribute[methodRuleAtts.Length + classRuleAtts.Length]; methodRuleAtts.CopyTo(allRuleAtts, 0); classRuleAtts.CopyTo(allRuleAtts, methodRuleAtts.Length); } else // just retrieve class-level assertion attributes allRuleAtts = (AssertAttribute[])classRuleAtts; // save into extension context ctx.AssertAttributes = allRuleAtts;

We then iterate through all of the harvested Assert attributes and build the XPath string expressions. We're wrapping the supplied expression with the XPath Boolean function to ensure that it returns a Boolean value, regardless of the expression. We'll simply apply a logical and operator on all of the Boolean expressions together to make sure that they all succeed when evaluating the complete expression. We also want to cache each individual compiled expression for future use when inspecting errors. The entire process is illustrated in Figure 6. Then, of course, we cache the complete compiled expression so we can do a quick check inside of ProcessMessage during each future invocation.

Figure 6 Building and Compiling the Cache Expressions

// generate, compile, and cache expressions for future use StringBuilder completeExpression = new StringBuilder(); string and = ""; foreach(AssertAttribute ra in allRuleAtts) { string rule = String.Format("boolean({0})", ra.Rule); // cache compiled expression for future use ra.Expression = nav.Compile(rule); ra.Expression.SetContext(ns); completeExpression.Append(and); completeExpression.Append(rule); and = " and "; } if (completeExpression.Length != 0) { ctx.CompleteRuleExpression = nav.Compile( completeExpression.ToString()); ctx.CompleteRuleExpression.SetContext(ns); }

Extending ProcessMessage

In order to evaluate XPath expressions, ProcessMessage needs to load the request stream into an XPathDocument object. If the user has enabled schema validation, we'll load the XPathDocument object from an XmlValidatingReader; otherwise we'll use an XmlTextReader directly. Once the XPathDocument is loaded, we know its schema is valid because the load operation didn't throw an exception. Then we're ready to evaluate the complete XPath expression, as illustrated here:

XPathDocument doc = new XPathDocument(reader); XPathNavigator nav = doc.CreateNavigator(); if (false == (bool)nav.Evaluate(_context.CompleteRuleExpression)) { // generate error response here }

If the expression results in true, the extension allows the processing to continue onto the WebMethod; otherwise the extension throws a SoapException.

To help the caller figure out what went wrong, we want to identify each business rule that failed. Inside the if block, we'll iterate through the ValidationContext.AssertAttributes array and evaluate each one to see where the error occurred. While doing so, we'll generate a custom detail element to use the SoapException (detailElement), as shown here:

SoapException error = new SoapException("Business rules failed validation", SoapException.ClientFaultCode, HttpContext.Current.Request.Url.AbsoluteUri, detailElement);

If you'd like more information on how detailElement was generated, see the sample code. Figure 7 shows a typical SOAP response that contains the custom detail element specifying exactly which business rules failed.

Figure 7 SOAP Response for Failed Rules

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <soap:Fault> <faultcode>soap:Client</faultcode> <faultstring>Business rules failed validation</faultstring> <faultactor>https://localhost/tests/test2.asmx</faultactor> <detail> <dm:failedAssertions xmlns:s="https://schemas.xmlsoap.org/soap/envelope/" xmlns:t="https://www.example.org/tests/test2" xmlns:dm="https://www.develop.com/web/services/"> <dm:assert> <dm:expression>(//t:length >//t:width)</dm:expression> <dm:description>The length must be greater than the width </dm:description> </dm:assert> </dm:failedAssertions> </detail> </soap:Fault> </soap:Body> </soap:Envelope>

With all of these enhancements in place, we have the makings of a declarative business rule engine that is flexible and easy to use. Although clients will find out through the SOAP Fault details when a given business rule fails, we need a way to inform developers ahead of time about our business rule requirements.

Documenting Business Rules

We now have a way to declaratively add business rules by decorating classes and methods with [Validate] and [Assert], and we know that the underlying code will automatically enforce the rules. Anyone who reads our code can see what constraints apply to the WebMethod without having to understand the code that implements it. Most XML Web Service clients, however, won't have access to the source code, so they need an alternate form of documentation. Most developers provide this in human-readable form, such as an HTML page or Microsoft® Word file. Keeping a manually produced document in sync with the code is difficult and prone to errors.

WSDL provides a standard language for describing Web Services. A WSDL document contains all the details necessary to implement or consume a Web Service. Most of today's Web Services toolkits can read a WSDL file and generate the code required to consume or implement a Web Service. Wsdl.exe provides this functionality in the Microsoft .NET Framework SDK and Apache's WSDL2Java (Axis) provides equivalent functionality for programmers working in Java.

The elements that WSDL uses to describe a Web Service fall into two major categories: WSDL language constructs and extensibility elements. WSDL elements come from the WSDL namespace (https://schemas.xmlsoap.org/wsdl) and are used to describe only the most generic, abstract parts of a Web Service. Extensibility elements (from a namespace outside the WSDL namespace) provide more concrete information about the Web Service's behavior. Figure 8 shows a WSDL fragment with both types of elements.

Figure 8 WSDL Components

Figure 8** WSDL Components **

The service element (from the WSDL namespace) doesn't tell us much other than there is a Web Service named ComplexMath. It is used as a reference to the rest of the information about the ComplexMath Web Service. The address element (from the WSDL/SOAP namespace) is an extensibility element that tells us that we can communicate with ComplexMath at the https://math.com/convolve endpoint.

The WSDL specification makes it possible for developers to add ad hoc extensibility elements whose meaning is mutually understood by both the implementer and consumer of a Web Service. We want to leverage this flexibility to add extensibility elements that describe our validation attributes.

Once we've done this, we can extend the WebMethod infrastructure to automatically generate the WSDL file with our extensibility elements included, guaranteeing that it will be in sync with our original class definition. Then we want to ensure that the tools that generate proxies take into account our extensibility elements so that they are also automatically in sync with the Web Service.

WSDL Extensibility Elements

Mapping our attributes to extensibility elements is fairly straightforward. The extensibility elements should contain all of the information that is provided by our validation attributes. We will qualify our extensibility elements with the https://www.develop.com/web/services/ namespace.

We have three attributes to map to WSDL extensibility elements: AssertNamespaceBindingAttribute, AssertAttribute, and ValidationAttribute. The first one can only be applied to a class, but more than one can be applied. A single extensibility element that acts as a container can be used to hold elements for each namespace binding, as shown in Figure 9. The validationNS element contains a namespaces element which holds a namespace element for each AssertNamespaceBindingAttribute.

Figure 9 Namespace Attributes/Extensibility Mapping

The ValidationAttribute and multiple AssertAttributes can be applied to a class or method. These can be combined into a single extensibility element named validation, as shown in Figure 10.

Figure 10 Validation Attribute/Extensibility Mapping

WSDL Extensibility Points

Now that we have extensibility elements that capture the information in our attributes, we must decide where to use them in the WSDL file itself. A full explanation of the WSDL language is beyond the scope of this article, but to give you an idea, Figure 11 lists the five primary WSDL elements that are used to describe a Web Service.

Figure 11 Primary WSDL Elements

WSDL Element Description
<service> Enumerates endpoints (to access service)
<binding> Binds operations to access protocol
<portType> Associates operations with messages
<message> Associates messages with types
<types> Type definitions for messages

The binding element describes the concrete details surrounding how to use a set of operations (such as portType). Optionally, each operation in the binding may have a single input or output message, and any number of fault messages associated with it. The binding itself says nothing about what the messages look like and only enumerates the fact that they exist. Figure 12 shows a simple WSDL binding element and its extensibility points.

Figure 12 WSDL Binding Element

Figure 12** WSDL Binding Element **

This binding associates a single operation, Add, with the HTTP transport protocol. It tells us that there is an input and output message associated with the Add operation. It does not tell us the format of these messages, but it does tell us that they will be defined by some XML Schema.

The WSDL binding element has an extensibility point that comes just before the first operation element in the binding. You can include any number of elements (not from the WSDL namespace) here. Extensibility elements included at this location apply to all operations in the binding. The WSDL input element also contains an extensibility point. It may contain any number of elements (again, not from the WSDL namespace). Extensibility elements included here only apply to the input element. The output element has a corresponding extensibility point.

With this information we know where to place our extensibility elements. Figure 13 shows the equivalences between a WebMethod class and the WSDL binding element. For simplicity, the examples shown in this article do not have any return attributes. In practice, however, it might be a good idea to use return attributes to ensure the implementation produces proper results.

Figure 13 Class Attributes/Binding Extensibility

Figure 13** Class Attributes/Binding Extensibility **

Let's look at a complete example. Figure 14 shows a typical class that implements a Web Service. The ValidationAttribute and AssertAttributes can be applied to either a class or to individual methods, but in the end they cause validation/assertion processing to occur on the methods within a class. Applying them to the class causes validation/assertion processing to occur for all the methods in the class. To simplify the implementation, ValidationAttribute and AssertAttribute declarations on the class will simply be added to the declarations on each method. That way, there is a single extensibility point in the input element of the binding element. Figure 15 shows the binding element for this class.

Figure 15 Binding for Typical Web Service Class

<binding name="SimpleTestsSoap" type="s0:SimpleTestsSoap" xmlns="https://schemas.xmlsoap.org/wsdl"> <soap:binding transport="https://schemas.xmlsoap.org/soap/http" style="document" /> <validatioNS xmlns="https://www.develop.com/web/services/"> <namespaces> <namespace> <prefix>t</prefix> <namespace> https://www.example.org/tests/test2 </namespace> </namespace> <namespace> <prefix>s</prefix> <namespace> https://schemas.xmlsoap.org/soap/envelope/ </namespace> </namespaces> </validationNS> <operation name="CalcArea2"> <soap:operation style="document" soapAction="https://www.example.org/tests/test2/ CalcArea2" /> <input> <soap:body use="literal" /> <validation xmlns:s="https://schemas.xmlsoap.org/soap/envelope/" xmlns:t="https://www.example.org/tests/test2" xmlns="https://www.develop.com/web/services/" SchemaValidation="true"> <assertions> <assert> <expression>//t:width &gt;= 0</expression> <description> width must be greater than 0 </description> </assert> <assert> <expression> s:Envelope/s:Body/t:CalcArea2/t:length &gt;= /s:Envelope/s:Body/t:CalcArea2/t:width </expression> <description> length must be greater than or equal to width </description> </assert> <assert> <expression> (//t:length * //t:width) &gt; 100 </expression> <description> Area must be greater than 100 </description> </assert> </assertions> </validation> </input> </binding>

Figure 14 Web Service Class

[AssertNamespaceBinding("s", "https://schemas.xmlsoap.org/soap/envelope/")] [AssertNamespaceBinding("t", "https://www.example.org/tests/test2")] [Assert(@"//t:width > 0", "width must be greater than 0")] public class SimpleTests : System.Web.Services.WebService { [WebMethod] [Validation] [Assert("/s:Envelope/s:Body/t:CalcArea2/t:length >= /s:Envelope/s:Body/t:CalcArea2/t:width", "length must be greater than or equal to width")] [Assert("(//t:length * //t:width) > 100", "area must be greater than 100")] public double CalcArea2(double length, double width) { return length * width; } }

WSDL Generation, SoapExtensionReflector

We have extended WSDL to include our validation attribute information. In order to make this really useful we need to hook into the WebMethod infrastructure that automatically creates WSDL and include our extensibility elements.

You can hook into the ASP.NET WSDL generator by configuring it to use a class derived from SoapExtensionReflector. Simply add an entry to the soapExtensionReflectorTypes element in the web.config file, as shown here:

<system.web> <webServices> <soapExtensionReflectorTypes> <add type= "DevelopMentor.Web.Services.ValidationExtensionReflector, DevelopMentor.Web.Services, Version=1.0.0.0, PublicKeyToken=09381596d5226568, Culture=neutral"/> </soapExtensionReflectorTypes> </webServices> </system.web>

When ASP.NET creates a WSDL file, it first checks this section of web.config and makes an instance of each SoapExtensionReflector class listed. It then calls the SoapExtensionReflector.ReflectMethod once for each WebMethod. The ReflectMethod implementation has access to the WSDL object model as well as the MethodInfo for the particular method being reflected.

Hence, we can write a ReflectMethod implementation that adds our new WSDL extensibility elements to the WSDL object model based on the validation attributes found for the reflected method. In order to design the SoapExtensionReflector class, we create a class that derives from SoapExtensionReflector and override the ReflectMethod. When ReflectMethod is invoked, we use the ReflectionContext property of the base class in order to gain access to the MethodInfo and binding element for the method being processed, as shown in Figure 16.

Figure 16 Gaining Access to the MethodInfo

public class MyReflector : SoapExtensionReflector { public override void ReflectMethod() { // MethodInfo for method being processed MethodInfo mi = ReflectionContext.Method; // binding for method being processed Binding binding = ReflectionContext.Binding; // input operation for method being processed OperationBinding input = ReflectionContext.OperationBinding.Input; // Write code to reflect against method, find // validation attributes, and insert custom // extensibility elements } ••• }

In ReflectMethod we can use Binding.Add and Input.Add to add extensibility elements at the appropriate location. However, the extensibility elements that we add must be derived from ServiceDescriptionFormatExtension and must be properly marked with the XmlFormatExtensionAttribute to identify their usage. Consider the class shown here:

[XmlFormatExtension("MyExstension", "urn:myNamespace", typeof(Binding)] public class MyExtensibilityElement : ServiceDescriptionFormatExtension { // include data members here that will be // part of your extensibility element }

XmlFormatExtension is applied to the MyExtensibilityElement class, which represents a WSDL extensibility element. Its arguments identify the name of the extensibility element as "MyExtension" from the urn:myNamespace. It also marks this extensibility element for use within a WSDL binding element.

The XML format of the extensibility element is the result of using XmlSerializer against the ServiceDescriptionFormatExtension-derived object. You have to add XmlSerializer attributes to get the exact XML format that you desire. Look at the ValidationFormatExtension and NSFormatExtension classes in the complete sample project to see the details of how the XmlSerializer was configured to get the extensibility element formats we require.

Consuming WSDL

Our WSDL file now has all the validation information that was contained in the original source file. The last step of the development process is to build a proxy that accesses the Web Service. Both wsdl.exe and the Visual Studio® .NET "Add Web Reference" feature use .NET Framework classes to read WSDL and generate proxy classes. These classes also have hooks that enable their behavior to be modified.

Getting attributes into proxy code completes the documentation loop; that is, all components involved with the Web Service now contain the same documentation, which is automatically synchronized. We can even carry the process one step further by forcing the proxy code to perform the same validation checks on the messages before it sends them to the Web Service.

The SoapExtensionAttributes that are applied to WebMethods can also be applied to proxy classes generated by wsdl.exe or Visual Studio .NET. The effect is the same since the SoapExtension classes get to take part in the processing of the messages the proxy sends. This means we can use the same classes, with some minor modifications, to extend the client's behavior.

SoapExtensionImporter

To modify the behavior of wsdl.exe and Visual Studio .NET, we must design a SoapExtensionImporter-derived class and configure the client systems to use it. There is only one method that we must override: ImportMethod. During proxy generation it is called once for each operation found in the WSDL binding element.

When ImportMethod is called, we have access to two important pieces of information: a modifiable collection of attributes (in the proxy code being generated) and an object model of the WSDL file that is being processed:

public class MyReflector : SoapExtensionReflector { public override ImportMethod( CodeAttributeDeclarationCollection metadata) { SoapProtocolImporter importer = ImportContext; // implementation follows } ••• }

ImportContext, which comes from the SoapExtensionImporter base class, gives access to the WSDL file through its Service property and to the generated class through its CodeTypeDeclaration property. The code that implements the method can be accessed through the supplied metadata parameter.

To configure client systems to use our SoapExtensionImporter we must modify the machine.config file. We do this by adding the class name to the system.web/webServices/soapExtensionImporterTypes element, as shown here:

<configuration> <system.web> <webServices> <soapExtensionImporterTypes> <add type= "MyImporter, MyAssembly, Version=1.0.0.0, locale=neutral, PublicKeyToken=a4...9/> </soapExtensionImporterTypes> </webServices> </system.web> </configuration>

There are two important things to keep in mind when registering a SoapExtensionImporter. One is that a full assembly name must be used and the assembly itself must be added to the Global Assembly Cache (GAC) which requires that it's signed. The second thing to remember is that every time wsdl.exe runs, your extension will be used. This means your implementation better be well thought out and forgiving.

When MethodImport is invoked, there are two places where you might want to add attributes: in the method for the operation being processed and in the class that contains the methods for all of the operations. Since only MethodImport is invoked, you will have to be able to detect when the first method is being processed and use it in order to add attributes to the proxy class.

When MethodImport is called, you can inspect the object model of the WSDL to determine what extensibility elements have been added to the binding being processed, since each binding has a collection of extensibility elements associated with it. The collection itself has a Find method which can be used to quickly reference an extensibility element. The following code illustrates how to obtain the "MyExtension" extensibility element from the "urn:myNamespace" namespace:

••• XmlElement extensibility = ImportContext.Binding.Extensions.Find ("MyExtension", "urn:myNamespace"); •••

Once you've determined that the method has an extensibility element, you can add the corresponding attributes to the proxy class. To do this you must create an instance of the CodeAttributeDeclaration class. The constructor requires the name of the attribute you will be adding:

••• CodeAttributeDeclaration attr = new CodeAttributeDeclaration(typeof(MyAttribute).FullName); •••

Once you have constructed a CodeAttributeDeclaration you will need to add any necessary arguments. You can use this class' Arguments collection property and its corresponding Add method to provide additional instances of CodeAttributeArgument. The constructor for the CodeAttributeArgument class requires the name of the argument and its value. Once all of the arguments have been added to the CodeAttributeDeclaration, it can be added to the method in the proxy. This is accomplished by using the CustomAttribtutes property of the CodeTypeDeclaration property, as shown here:

ImportContext.CodeTypeDeclaration.CustomAttributes(attr);

For more information, take a look at the complete implementation of the ValidationExtensionImporter class in the sample project.

Client-side SoapExtension Processing

Applying SoapExtensionAttributes to the methods of a proxy class has the same effect as applying them to a WebMethod. These attributes make it possible to take part in the processing of the messages sent to and from a Web Service. In order to be able to check the messages against the client-side [Validation] and [Assert] attributes, we need to be able to inspect the message just before it's sent over the wire.

To perform the actual validation and assert checking, we need to trap the message stream and process it in ProcessMessage. Our SoapExtension class gives us a way to do this through the overridable ChainStream method. Each time a proxy method is invoked, ChainStream is called twice by the runtime: once just before it begins to build the request message, and once just before it begins to process the response message returned by the Web server.

The ChainStream method allows us to substitute our own stream for the one that the runtime would have used. We will provide a MemoryStream for the one being used to build the request message. This way, when the Framework creates the request message, it will write it to our stream. We can then perform validation on the MemoryStream (inside ProcessMessage). If it passes validation, we'll forward the contents of our stream onto the stream originally used by the runtime.

To do all this, our ChainStream implementation constructs a memory stream, returns a reference to it, and saves the original stream that was passed when ChainStream was called. Since ChainStream is called twice, it will have to keep track of which call it's processing. It should only substitute the stream on the first call, which is the one that happens just before it builds the request message (see Figure 17).

Figure 17 Overriding ChainStream

class public ValidationExtension : SoapExtension { MemoryStream requestStream; bool firstChain = true; •••// other methods omitted for brevity public override Stream ChainStream(Stream stream) { if(firstChain) { firstChain=false; requestStream = new MemoryStream(); return requestStream; } else { return stream; } } }

Once the request has been captured, it can be processed and copied to the original requestStream. With this mechanism in place, the proxy object performs validation and XPath assertion processing on the stream in ProcessMessage (just like with WebMethods on the server side), before actually sending the message. If the message passes validation, it's allowed to pass through; otherwise the proxy automatically generates a SoapException without wasting a round-trip. You can see the full implementation of the ValidationExtension in the complete sample project.

Conclusion

The WebMethod infrastructure provides a powerful extensibility framework for adding additional user-defined behavior. We were able to add XML Schema validation and XPath assertions to any WebMethod through various extension classes. We also made it possible to export the validation descriptions through WSDL, allowing the documentation to stay completely in sync with the code. Using these techniques makes it possible to enforce real-world business rules in the heterogeneous environment of XML-based Web Services.

For related articles see:
Web Services: Extend the ASP.NET WebMethod Framework by Adding XML Schema Validation

Aaron Skonnard is an instructor and researcher at DevelopMentor, where he develops the XML and Web Service-related curriculum. Aaron coauthored Essential XML Quick Reference (Addison-Wesley, 2001) and Essential XML (Addison-Wesley, 2000).

Dan Sullivan is an independent consultant, and an instructor and course author at DevelopMentor. He has worked in all facets of computing, from designing processors to writing applications and system software. Dan now focuses on XML and developing Web Services and can be contacted at dsullivan@net1plus.com.