Condividi tramite


How to: Perform Message Validation with Schema Validation in WCF

patterns & practices Developer Center

Applies To

  • Microsoft Windows Communication Foundation (WCF) 3.5
  • Microsoft .NET Framework 3.5
  • Microsoft Visual Studio 2008

Summary

This how-to article shows you how to perform message validation using a schema in WCF. You will learn how to create a custom client message inspector and dispatcher message inspector that can be used to validate messages on both the server and the client.

Contents

  • Objectives
  • Overview
  • Summary of Steps
  • Step 1: Create a Sample WCF Service
  • Step 2: Configure the WCF Service to Use wsHttpBinding with Windows Authentication and Message Security
  • Step 3: Create the Schema to Validate the Message
  • Step 4: Create a Windows Class Library Project That Will Contain the Three Classes Necessary for Schema Validation
  • Step 5: Create a Class That Implements the Schema Validation Logic
  • Step 6: Create a Class That Implements a Custom Endpoint Behavior
  • Step 7: Create a Class That Implements a Custom Configuration Element
  • Step 8: Add the Custom Behavior to the Configuration File
  • Step 9: Create an Endpoint Behavior and Map It to Use the Custom Behavior
  • Step 10: Configure the Service Endpoint to Use the Endpoint Behavior
  • Step 11: Test the Schema Validator
  • Deployment Considerations
  • Additional Resources

Objectives

  • Learn how to create a custom configuration element that will allow exposing the custom endpoint behavior in the configuration file.
  • Learn how to create a custom endpoint behavior that will consume the client and dispatcher message inspectors.
  • Learn how to create custom client and dispatcher message inspectors to validate messages using schemas.

Overview

Message validation represents one line of defense in the protection of your WCF application. With this approach, you validate messages using schemas to protect WCF service operations from attack by a malicious client. Validate all messages received by the client to protect the client from attack by a malicious service. Message validation makes it possible to validate messages when operations consume message contracts or data contracts, which cannot be done using parameter validation. Message validation allows you to create validation logic inside schemas, thereby providing more flexibility and reducing development time. Schemas can be reused across different applications inside the organization, creating standards for data representation. Additionally, message validation allows you to protect operations when they consume more complex data types involving contracts representing business logic.

To perform message validation, you first build a schema that represents the operations of your service and the data types consumed by those operations. You then create a .NET class that implements a custom client message inspector and custom dispatcher message inspector to validate the messages sent/received to/from the service. Next, you implement a custom endpoint behavior to enable message validation on both the client and the service. Finally, you implement a custom configuration element on the class that allows you to expose the extended custom endpoint behavior in the configuration file of the service or the client.

Summary of Steps

  • Step 1: Create a Sample WCF Service
  • Step 2: Configure the WCF Service to Use wsHttpBinding with Windows Authentication and Message Security
  • Step 3: Create the Schema to Validate the Message
  • Step 4: Create a Windows Class Library Project That Will Contain the Three Classes Necessary for Schema Validation
  • Step 5: Create a Class That Implements the Schema Validation Logic
  • Step 6: Create a Class That Implements a Custom Endpoint Behavior
  • Step 7: Create a Class That Implements a Custom Configuration Element
  • Step 8: Add the Custom Behavior to the Configuration File
  • Step 9: Create an Endpoint Behavior and Map It to Use the Custom Behavior
  • Step 10: Configure the Service Endpoint to Use the Endpoint Behavior
  • Step 11: Test the Schema Validator

Step 1: Create a Sample WCF Service

In this step, you create a WCF service in Visual Studio, hosted in an Internet Information Services (IIS) virtual directory.

  1. In Visual Studio, on the menu, click File -> New Web Site.

  2. In the New Web Site dialog box, in the Templates section, select WCF Service. Make sure that the Location is set to Http.

  3. In the New Web Site dialog box, set the new Web site address to https://localhost/WCFTestSchemaValidation and then click OK.

  4. Create a data contract with the CustomerData class. This data contract will be consumed by the operation of your service. To do this, in Visual Studio, double-click the IService.cs file, copy the following code snippet, and paste it into the bottom of the IService.cs file:

    [DataContract(Namespace = "http://Microsoft.PatternPractices.WCFGuide")]
    public class CustomerData
    {
        [DataMember]
        string text;
        [DataMember]
        int CustomerID;
    
        public string Text
        {
            get { return text; }
            set { text = value; }
        }
    
        public int customerid
        {
            get { return CustomerID; }
            set { CustomerID = value; }
        }
    }
    
  5. Change the definition of the GetData operation contract to use the CustomerData type. To do this, double-click IService.cs, go to the top of the file, and replace the previous definition with the new one as follows.

    Previous operation contract:

    [OperationContract]
    string GetData(int value);
    

    New operation contract:

    [OperationContract]
    CustomerData GetData(CustomerData CustomerInfo);
    
  6. Change the implementation of the GetData operation contract to use the CustomerData type. Double-click IService.cs, go to the top of the file, and replace the previous definition with the new one as follows.

    Previous operation contract implementation:

    public string GetData(int value)
    {
        return string.Format("You entered: {0}", value);
    }
    

    New operation contract implementation:

    public CustomerData GetData(CustomerData CustomerInfo)
    {
       CustomerData CustomerInfoResponse = new CustomerData();
       CustomerInfoResponse.Text = CustomerInfo.Text;
       CustomerInfoResponse.customerid = CustomerInfo.customerid+1;
       return CustomerInfoResponse;
    }
    
  7. Add the namespace definition to conform to the schema you are going to create. Double-click IService.cs and add the string (Namespace = "http://Microsoft.PatternPractices.WCFGuide") after the ServiceContract entry in that file. Your service contract entry will look like the following code fragment:

    [ServiceContract(Namespace = "http://Microsoft.PatternPractices.WCFGuide")]
    

Step 2: Configure the WCF Service to Use wsHttpBinding with Windows Authentication and Message Security

By default, your WCF service will be configured to use wsHttpBinding with message security and Windows authentication. Verify that your web.config configuration file looks as follows:

…
<services>
  <service name="Service" behaviorConfiguration="ServiceBehavior">
    <!-- Service Endpoints -->
    <endpoint address="" binding="wsHttpBinding" contract="IService">
      <identity>
        <dns value="localhost"/>
      </identity>
    </endpoint>
    <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> 
  </service>
</services> 
…

Step 3: Create the Schema to Validate the Message

In this step, you create the schema to validate the message.

  1. Right-click the https://localhost/WCFTestSchemaValidation project and then click Add New Item.

  2. Select Xml Schema from the Visual Studio installed templates and enter the name SchemaValidation in the Name text box. Select Visual C# as the language and then click Add.

  3. In the Visual Studio editor, delete the entire contents of this file. Copy the following schema definition and paste it into the file:

    <?xml version="1.0" encoding="utf-8"?>
    <xs:schema elementFormDefault="qualified" targetNamespace="http://Microsoft.PatternPractices.WCFGuide" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://Microsoft.PatternPractices.WCFGuide">
      <xs:element name="GetData">
        <xs:complexType>
          <xs:sequence>
            <xs:element minOccurs="1" name="CustomerInfo" nillable="false" type="tns:CustomerData" />
          </xs:sequence>
        </xs:complexType>
      </xs:element>
      <xs:complexType name="CustomerData">
        <xs:sequence>
          <xs:element name="CustomerID" type="tns:CustIDLimiter">
          </xs:element>
          <xs:element name="text" type="tns:CustomerN">
          </xs:element>     
        </xs:sequence>
      </xs:complexType>
      <xs:simpleType name="CustomerN">
        <xs:restriction base="xs:string">
          <xs:minLength value="1" />
          <xs:maxLength value="5" />
        </xs:restriction>
      </xs:simpleType>
      <xs:simpleType name="CustIDLimiter">
        <xs:restriction base="xs:int">
          <xs:minInclusive value="1" />
          <xs:maxInclusive value="5" />
        </xs:restriction>
      </xs:simpleType>
      <xs:element name="GetDataResponse">
        <xs:complexType>
          <xs:sequence>
            <xs:element minOccurs="1" name="GetDataResult" nillable="false" type="tns:CustomerData" />
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:schema>
    

    The schema first defines the GetData operation contract that takes the CustomerData type as a parameter being instantiated by CustomerInfo being passed to the operation call. CustomerData must exist and cannot be null.

    <?xml version="1.0" encoding="utf-8"?>
    <xs:schema elementFormDefault="qualified" targetNamespace="http://Microsoft.PatternPractices.WCFGuide" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://Microsoft.PatternPractices.WCFGuide">
      <xs:element name="GetData">
        <xs:complexType>
          <xs:sequence>
            <xs:element minOccurs="1" name="CustomerInfo" nillable="false" type="tns:CustomerData"/>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    

    CustomerData is a complex type that has CustomerID and text as data members. CustomerID and text are of type CustIDLimiter and CustomerN.

       <xs:complexType name="CustomerData">
        <xs:sequence>
          <xs:element name="CustomerID" type="tns:CustIDLimiter">
          </xs:element>
          <xs:element name="text" type="tns:CustomerN">
          </xs:element>     
        </xs:sequence>
      </xs:complexType>
    

    CustomerN and CustIDLimiter are just schema facets: they are simple types (string and integer) that serve to limit the length and value of the string and integer. In the fragment below, the string is limited to 1 to 5 characters in length, and the integer is limited to range values from 1 to 5.

      <xs:simpleType name="CustomerN">
        <xs:restriction base="xs:string">
          <xs:minLength value="1" />
          <xs:maxLength value="5" />
        </xs:restriction>
      </xs:simpleType>
      <xs:simpleType name="CustIDLimiter">
        <xs:restriction base="xs:int">
          <xs:minInclusive value="1" />
          <xs:maxInclusive value="5" />
        </xs:restriction>
      </xs:simpleType>
    </xs:schema> 
    

    The GetDataResponse element is a response from GetData. It returns a CustomerInfoResponse with type CustomerData, so the response will be also tested.

      <xs:element name="GetDataResponse">
        <xs:complexType>
          <xs:sequence>
            <xs:element minOccurs="1" name="GetDataResult" nillable="false" type="tns:CustomerData" />
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:schema>
    

Step 4: Create a Windows Class Library Project That Will Contain the Three Classes Necessary for Schema Validation

In this step, you create a Microsoft Windows class library project that will include three classes necessary for the schema validation:

  • One class to implement the schema validation logic
  • A second class to implement the endpoint behavior that will use the schema validation class
  • A third class to implement a behavior extension so that the validator will be visible in the service and client configuration files

Perform the following steps to create a Windows class library project with these three classes:

  1. Open a new instance of Visual Studio, leaving your WCF service solution open.

  2. In the new instance of Visual Studio, click File, click New, and then click Project.

  3. Expand Visual C#, click Windows, and then select Class Library.

  4. In the Name field, type MySchemaValidationClass and then click OK.

  5. In the Solution Explorer, right-click References and then click Add Reference. Click the .NET tab, click System.ServiceModel, and then click OK.

  6. In the Solution Explorer, right-click References and then click Add Reference. Click the .NET tab, click System.Configuration, and then click OK.

  7. Open the Class1.cs file and rename the class name from Class1 to Validation.

  8. Add the following using statements to the top of the Class1.cs file:

    using System.Configuration;
    using System.ServiceModel;
    using System.ServiceModel.Configuration;
    using System.ServiceModel.Description;
    using System.ServiceModel.Dispatcher;
    

Step 5: Create a Class That Implements the Schema Validation Logic

In this step, you create a new class, derived from the IClientMessageInspector and IDispatchMessageInspector interfaces, to implement the schema validation logic for both the client and the dispatcher.

The newly created class implements the AfterReceiveRequest(), BeforeSendReply(), BeforeSendRequest(), and AfterReceiveReply() methods and has the following characteristics:.

  • On the dispatcher: AfterReceiveRequest will happen when inbound messages are received by the dispatcher, before the operation is invoked and deserialization of messages has occurred. If the message is encrypted, decryption will take place first.

    BeforeSendReply will happen when outbound messages are to be sent back to the client. It will happen after the operation is invoked, and after serialization has occurred. If the message is encrypted, encryption will not take place.

  • On the client: BeforeSendRequest will happen when outbound messages are sent by the client, after serialization has occurred. If the message is encrypted, encryption will not take place.

    AfterReceiveReply will happen when inbound messages are received by the client before deserialization of the message has occurred. If the message is encrypted, decryption will take place first.

  • ValidateMessage: ValidateMessage will validate the message with regard to the schema definition. If validation succeeds, a new message is constructed and returned to the caller. On the dispatcher side, a new message is returned either before the operation is invoked or before a response is sent to the client. On the client side, ValidateMessage is called before sending the message to the service or before returning to the application.

In the following example, a simple schema validation logic is implemented by simply traversing the XmlReader. If validation fails, a fault exception or a message is returned to the client.

Copy and paste the following code snippet to the class1.cs file:

public class SchemaValidation
    {
       public class SchemaValidationMessageInspector : IClientMessageInspector,IDispatchMessageInspector
        {
            XmlSchemaSet schemas;
            public SchemaValidationMessageInspector(XmlSchemaSet schemas)
            {
                this.schemas = schemas;
            }
            void validateMessage(ref System.ServiceModel.Channels.Message message)
            {
                XmlDocument bodyDoc = new XmlDocument();
                bodyDoc.Load(message.GetReaderAtBodyContents());
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.Schemas.Add(schemas);
                settings.ValidationType = ValidationType.Schema;
                XmlReader r = XmlReader.Create(new XmlNodeReader(bodyDoc), settings);
                while (r.Read()) ; // do nothing, just validate
                // Create new message 
                Message newMsg = Message.CreateMessage(message.Version, null,
                    new XmlNodeReader(bodyDoc.DocumentElement));
                newMsg.Headers.CopyHeadersFrom(message);
                foreach (string propertyKey in message.Properties.Keys)
                    newMsg.Properties.Add(propertyKey, message.Properties[propertyKey]);
                // Close the original message and return new message 
                message.Close();
                message = newMsg; 
            }

            object IDispatchMessageInspector.AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
            {
                try{
                    validateMessage(ref request);
                }      
            catch (FaultException e)
            {
                throw new FaultException<string>(e.Message);
            }
            return null;

            }

            void IDispatchMessageInspector.BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
            {
                try
                {
                    validateMessage(ref reply);                
                }
                catch (FaultException fault)
                {
                    // if a validation error occurred, the message is replaced
                    // with the validation fault.
                    reply = Message.CreateMessage(reply.Version, new FaultException("validation error in reply message").CreateMessageFault() , reply.Headers.Action);
                }

            }

            void IClientMessageInspector.AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
            {
                validateMessage(ref reply);               
            }

            object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
            {
                validateMessage(ref request);
                return null;
            }            
        }
    }

Step 6: Create a Class That Implements a Custom Endpoint Behavior

In this step, you create a new class, derived from IEndpointBehavior, that implements a custom endpoint behavior.

The newly created class has the following characteristics:

  • It implements ApplyClientBehavior() to add the SchemaValidationMessageInspector to the client operation and enable client-side validation.
  • It implements ApplyDispatchBehavior() to add the SchemaValidationMessageInspector to the dispatch operation and enable service-side validation.
  • It verifies that it is enabled in the configuration before adding the SchemaValidationMessageInspector to the client or dispatch run time.

Copy and paste the following code snippet to the Class1.cs file, inside the Validation class that already exists:

  class SchemaValidationBehavior : IEndpointBehavior
        {
            private bool enabled;
            private XmlSchemaSet schemaSet; 


            internal SchemaValidationBehavior(bool enabled,XmlSchemaSet schemaSet)
            {
                this.enabled = enabled;
                this.schemaSet = schemaSet;
            }

            public bool Enabled
            {
                get { return enabled; }
                set { enabled = value; }
            }

            public void AddBindingParameters(ServiceEndpoint serviceEndpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
            { }

            public void ApplyClientBehavior(
              ServiceEndpoint endpoint,
              ClientRuntime clientRuntime)
            {
                //If enable is not true in the config we do not apply the Parameter Inspector
                if (false == this.enabled)
                {
                    return;
                }
                SchemaValidationMessageInspector inspector = new SchemaValidationMessageInspector(schemaSet);
                clientRuntime.MessageInspectors.Add(inspector);               
            }

            public void ApplyDispatchBehavior(
               ServiceEndpoint endpoint,
               EndpointDispatcher endpointDispatcher)
            {
                //If enable is not true in the config we do not apply the Parameter Inspector
                if (false == this.enabled)
                {
                    return;
                }
                SchemaValidationMessageInspector inspector = new SchemaValidationMessageInspector(schemaSet);
                endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);  
            }

            public void Validate(ServiceEndpoint serviceEndpoint)
            {

            }


        }

Step 7: Create a Class That Implements a Custom Configuration Element

In this step, you create a new class, derived from BehaviorExtensionElement, that implements a custom configuration element.

The newly created class has the following characteristics:

  1. It implements CreateBehavior() to create an instance of the SchemaValidationBehavior class. Inside the method, a schema set is initialized with the schema that reads the base directory of the application loading the SchemaValidation.xsd file created previously. The schema set is passed to the SchemaValidation behavior that will pass it to the schema inspector.
  2. It implements BehaviorType() to return the SchemaValidationBehavior type. This will allow the custom behavior to be exposed in the service or client configuration sections.
  3. It implements ConfigurationProperty to allow the behavior to be enabled or disabled in the WCF configuration files.

Copy and paste the following code snippet to the Class1.cs file, inside the Validation class that already exists:

 public class CustomBehaviorSection : BehaviorExtensionElement
{

            private const string EnabledAttributeName = "enabled";

            [ConfigurationProperty(EnabledAttributeName, DefaultValue = true, IsRequired = false)]
            public bool Enabled
            {
                get { return (bool)base[EnabledAttributeName]; }
                set { base[EnabledAttributeName] = value; }
            }

            protected override object CreateBehavior()
            {
               XmlSchemaSet schemaSet = new XmlSchemaSet();
               Uri baseSchema = new Uri(AppDomain.CurrentDomain.BaseDirectory);
               string mySchema = new Uri(baseSchema,"SchemaValidation.xsd").ToString();
               XmlSchema schema = XmlSchema.Read(new XmlTextReader(mySchema), null);
               schemaSet.Add(schema);
               return new SchemaValidationBehavior(this.Enabled,schemaSet);

            }

            public override Type BehaviorType
            {

                get { return typeof(SchemaValidationBehavior); }


            }
        }

Step 8: Add the Custom Behavior to the Configuration File

In this step, you add the custom behavior to the behavior element extension in the WCF configuration file so that it can be used by the WCF endpoint.

  1. Compile your schema validation class library solution to create MySchemaClassValidation.dll.

  2. Return to the original instance of Visual Studio that contains your WCF service solution.

  3. Right-click the WCF Web site project and then click Add Reference. Navigate to the folder containing MySchemaClassValidation.dll and click Add.

  4. Right-click web.config and then click Edit WCF configuration.

    If you do not see the Edit WCF Configuration option, on the Tools menu, click WCF Service Configuration Editor. Close the WCF Service Configuration Editor tool that appears. The option should now appear on the web.config context menu.

  5. Expand the Services node and the Extensions node and then click Behavior Element Extensions.

  6. Click New.

  7. In the Name field, type SchemaValidator

  8. Select the Type field, click the button that appears to the right, navigate to the folder containing MySchemaClassValidation.dll, and then double-click the .dll file.

  9. Double-click the type name MySchemaValidationClass.SchemaValidation+CustomBehaviorSection and then click OK.

  10. In the WCF Configuration Editor, click File and then click Save.

Verify that your configuration file contains the following:

<system.serviceModel>
  ...
<extensions>
      <behaviorExtensions>
        <add name="SchemaValidator" type="MySchemaValidationClass.SchemaValidation+CustomBehaviorSection, MySchemaValidationClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      </behaviorExtensions>
    </extensions>
 ... 
 <system.serviceModel>

Step 9: Create an Endpoint Behavior and Map It to Use the Custom Behavior

In this step, you create an endpoint behavior and map it to the custom behavior created in Step 7.

  1. In the WCF Configuration Editor, expand the Advanced node, right-click Endpoint Behavior, and then click New Endpoint Behavior Configuration.
  2. Select the new behavior and then in the Name field, type MyEndPointBehavior
  3. Click Add, select the SchemaValidator custom behavior, and then click Add again.
  4. In the WCF Configuration Editor, click File and then click Save.

Verify that your configuration file contains the following:

<behaviors>
  ...
<endpointBehaviors>
        <behavior name="MyEndPointBehavior">
          <SchemaValidator />
        </behavior>
      </endpointBehaviors>
 ...  
 </behaviors>

Step 10: Configure the Service Endpoint to Use the Endpoint Behavior

In this step, you configure the service to use the endpoint behavior to consume the custom validator.

  1. In the WCF Configuration Editor, expand the Service node and then expand Endpoints.
  2. Select the first [Empty Name] node.
  3. In the BehaviorConfiguration field, select MyEndPointBehavior.
  4. In the WCF Configuration Editor, click File and then click Save.

Verify that your configuration file contains the following:

<endpoint address="" behaviorConfiguration="MyEndPointBehavior"
          binding="wsHttpBinding" contract="IService">
          <identity>
            <dns value="localhost" />
          </identity>
</endpoint>

Step 11: Test the Schema Validator

In this step, you create a sample WCF client to test your validator.

  1. Right-click your WCF service solution, click Add, and then click New Project.

  2. In the Add New Project dialog box, in the Templates section. select Windows Forms Application.

  3. In the Name field, type Test Client and then click OK.

  4. Right-click your client project and then click Add Service Reference.

  5. In the Add Service Reference dialog box, set the Address field to https://localhost/WCFTestSchemaValidation and then click Go.

  6. Set the Namespace field to WCFTestService and then click OK.

  7. Open the designer for your new Windows form.

  8. Drag three text box controls into the designer.

  9. Drag a button control into the designer.

  10. Double-click the button to show the underlying code.

  11. In the code behind the button, create an instance of the WCF service proxy, and then call the GetData() method on your WCF service as follows:

       try
          {
                WCFTestService.CustomerData CustomerInfo = new WindowsFormsApplication1. WCFTestService.CustomerData();
                CustomerInfo.text = textBox1.Text;
                CustomerInfo.CustomerID = int.Parse(textBox2.Text);
                WCFTestService.ServiceClient proxy = new WindowsFormsApplication1. WCFTestService.ServiceClient();
                WCFTestService.CustomerData CustomerInfoResponse = proxy.GetData(CustomerInfo);
                textBox3.Text = CustomerInfoResponse.text;
                proxy.Close();
          }
    
          catch (FaultException ex)
          {
               textBox3.Text = ex.Message;
          }
    
  12. Right-click the client project and then click Set as Startup Project.

  13. Run the client application by pressing F5 or CTRL+F5 and then click the button.

  14. In textbox1, enter a string with a maximum length of 5 characters.

  15. In textbox2, enter an integer value between 1 and 4.

  16. Try entering values outside of these validation ranges and you will see that the application displays a validation error.

Deployment Considerations

Consider the following key points before deployment:

  • Do not divulge exception errors to clients in production. Instead, develop a fault contract and return it to your client inside AfterReceiveRequest().
  • Do not divulge exception errors after BeforeSendReply(). Instead, develop a fault contract and build an error message with the fault contract and return it to the client.
  • For client-side validation, follow the same steps detailed in this How To article, but instead use the app.config file of the client consuming the service.
  • Consider caching the schema for performance benefits.
  • Consider the more advanced schema validation logic provided in the download sample at https://msdn.microsoft.com/en-us/library/aa717047.aspx.

Additional Resources