Inside the Web Services Enhancements Pipeline

 

Tim Ewald
Microsoft Corporation

December 2002

Applies to:
   Microsoft® .NET
   SOAP messaging
   Web Services Enhancements 1.0 for Microsoft® .NET
   Microsoft® ASP.NET Web services

Summary: Describes how Web Services Enhancements 1.0 for Microsoft .NET works: how individual filters and pipelines of filters work, how to configure the default pipeline, how to build custom filters, and where DIME fits into the picture. (18 printed pages)

Download Web Services Enhancements 1.0 for Microsoft .NET.

Contents

Introduction
A Filter-centric Model
Working with Individual Filters
Working with Multiple Filters in a Pipeline
Custom Filters
What About DIME?
Conclusion
Related Resources

Introduction

Web Services Enhancements 1.0 for Microsoft .NET (WSE) is a class library that implements advanced Web services protocols. The architectural model of WSE is based on a pipeline of filters that process inbound and outbound SOAP messages. The filters can be integrated with the ASP.NET Web services infrastructure, or they can be used on their own. This paper explores how the WSE plumbing works from the bottom up, explaining how individual filters and pipelines of filters work, how to configure the default pipeline, how to build custom filters, and where DIME fits into the picture.

Note This article is a companion to Programming with Web Services Enhancements 1.0 for Microsoft .NET and assumes that you are familiar with the material covered there.

A Filter-centric Model

WSE is an engine for applying advanced Web service protocols to SOAP messages. This entails writing headers to outbound SOAP messages and reading headers from inbound SOAP messages. It may also require transforming the SOAP message body. For instance, encrypting an outbound message's body and decrypting an inbound message's body, as defined by the WS-Security specification. In WSE, this functionality is implemented with filters. Output filters write headers to messages. Input filters read headers from messages and check their validity. Both output and input filters may transform the contents of a message as well. Figure 1 illustrates the WSE filter model.

Figure 1. The filter model for Web Services Enhancements

Working with Individual Filters

The best way to learn how WSE uses filters is to start with a simple example. WSE provides a pair of filters that read and write timestamp headers, as defined in the WS-Security Addendum. Timestamp headers contain elements representing both message creation and expiration times, indicating the age of a message and the point at which it should be considered stale. The timestamp filters are defined in the Microsoft.Web.Services.Timestamp namespace. TimestampOutputFilter is, as the name suggests, an output filter that writes a timestamp header to a SOAP message. TimestampInputFilter is an input filter that reads a timestamp header from a SOAP message. Here are their respective definitions:

public class TimestampOutputFilter : SoapOutputFilter
{
  public override void ProcessMessage(SoapEnvelope envelope);
}

public class TimestampInputFilter : SoapInputFilter
{
  public override void ProcessMessage(SoapEnvelope envelope);
}

Each class has a single method, ProcessMessage, with a single parameter of type SoapEnvelope. The Microsoft.Web.Services.SoapEnvelope class is an extension of System.Xml.XmlDocument, the standard .NET XML DOM API. It has logic to verify that its contents of the document represent a valid SOAP message. It also has some shortcut methods and properties that create and access particular parts of a message, i.e., the Envelope, Header and Body elements.

Here is a simple application that uses the timestamp filters.

 
static void Main(string[] args)
{
  // Build SOAP message with nothing in it
  SoapEnvelope env = new SoapEnvelope();
  XmlElement body = env.CreateBody();
  env.Envelope.AppendChild(body);

  // Print out the original message
  Console.WriteLine("Original message:\n\n{0}\n", env.OuterXml);

  // Create a timestamp output filter
  TimestampOutputFilter tsOutput = new TimestampOutputFilter();
  
  // Process the message, write a timestamp header
  tsOutput.ProcessMessage(env);

  // Print out the output filtered message
  Console.WriteLine("Output filtered message:\n\n{0}\n", env.OuterXml);

  // Create a timestamp input filter
  TimestampInputFilter tsInput = new TimestampInputFilter();

  // Process the message, read the timestamp header
  tsInput.ProcessMessage(env);

  // Print out the input filtered message
  Console.WriteLine("Input filtered message:\n\n{0}\n", env.OuterXml);
}

This program creates a SoapEnvelope object and adds an empty message body. Then it creates a TimestampOutputFilter and uses it to process the SoapEnvelope, writing a timestamp header to the message. Finally, the program creates a TimestampInputFilter and uses it process the SoapEnvelope, reading the timestamp header from the message. In-between each step, the program prints the message's contents to the console. The (formatted) output is shown below.

Original message:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body/>
</soap:Envelope>

Output filtered message:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsu:Timestamp
      xmlns:wsu="https://schemas.xmlsoap.org/ws/2002/07/utility">
      <wsu:Created>2002-11-14T19:03:27Z</wsu:Created>
      <wsu:Expires>2002-11-14T19:08:27Z</wsu:Expires>
    </wsu:Timestamp>
  </soap:Header>
  <soap:Body />
</soap:Envelope>

Input filtered message:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
  </soap:Header>
  <soap:Body />
</soap:Envelope>

The original message contained an empty Body element, and nothing more. When the TimestampOutputFilter processed the message, it wrote a timestamp header with elements representing the message creation time and the message expiration time. When the TimestampInputFilter processed the message, it read the timestamp header. If the expiration time had passed, the TimestampInputFilter would have thrown an exception.

Using SoapContext to Communicate with a Filter

The behavior of the output filters is controlled through the Microsoft.Web.Services.SoapContext class. Each SoapContext object records particular protocol options—the presence of a username token or digital certificate, creation and expiration timestamps, routing paths, etc.—using a simple object model. The SoapEnvelope class has a Context property that is an instance of the SoapContext class. When a SoapEnvelope is processed by an output filter, it is the data in the SoapContext object that tells the output filter what to do.

For instance, TimestampOutputFilter actually sets the expiration time of a message by examining a property of a SoapEnvelope's SoapContext object, specifically, SoapEnvelope.Context.Timestamps.Ttl. If you wanted to set a message's expiration time of ten minutes, all you have to do is set this value, as in:

// build envelope
SoapEnvelope env = new SoapEnvelope();
... 
// set expiration to ten minutes in milliseconds
env.Context.Timestamps.Ttl = 600000;

The WSE Input filters also rely on the SoapContext class, but for a different purpose. When a SoapEnvelope object is processed by an input filter, its SoapContext object is updated to reflect the protocol headers the envelope contains. For instance, after TimestampInputFilter processes a message, the SoapEnvelope.Context.Timestamps.Created property reflects the message's creation time.

Working with Multiple Filters in a Pipeline

The timestamp input and output filters I showed you in the previous section are just two of the ten built-in filters that ship with WSE. Table 1 lists all the built-in filters, including the namespace they are defined in, the input and output filter type names, and a brief description of what they do.

Table 1. WSE Built-in filters in Web Services Enhancements

Namespace Input Filter Output Filter Purpose
Microsoft.Web.Services.Diagnostics TraceInputFilter TraceOutputFilter Write messages to log files to help with debugging
Microsoft.Web.Services.Security SecurityInputFilter SecurityOutputFilter Authentication, signature and encryption support
(WS-Security)
Microsoft.Web.Services.Timestamp TimestampInputFilter TimestampOutputFilter Timestamp support
(WS-Security)
Microsoft.Web.Services.Referral ReferralInputFilter ReferralOutputFilter Dynamic updates to routing paths
(WS-Referral)
Microsoft.Web.Services.Routing RoutingInputFilter RoutingOutputFilter Message routing
(WS-Routing)

The right-most column in Table 1 makes it clear that each pair of WSE filters is designed to do a specific thing. In general, you are likely to want to combine multiple input filters or output filters and apply them to a given SOAP message en masse. WSE provides the Microsoft.Web.Services.Pipeline class to facilitate this. Its definition is shown below.

public class Pipeline
{
  public Pipeline();
  public Pipeline(Pipeline p);
  public Pipeline(SoapInputFilterCollection inputFilters,
                  SoapOutputFilterCollection outputFilters);

  public SoapInputFilterCollection inputFilters { get; }
  public SoapOutputFilterCollection outputFilters { get; }

  public void ProcessInputMessage(SoapEnvelope envelope);
  public void ProcessOutputMessage(SoapEnvelope envelope);
}

Each Pipeline object encapsulates a collection of input filters and a collection of output filters, represented by instances of the Microsoft.Web.Services.SoapInputFilterCollection and Microsoft.Web.Services.SoapOutputFilterCollection classes, respectively. These collections are initialized on construction and exposed as properties as well. The Pipeline.ProcessInputMessage and Pipeline.ProcessOutputMessage methods simply iterate over the appropriate filter collection, passing the provided SoapEnvelope object to each one in turn.

Here is a simple program that uses the Pipeline class to process a message using multiple output filters. Specifically, it uses the built-in TraceOutputFilter and TimestampOutputFilter classes in WSE.

static void Main(string[] args)
{
  // build collection of input filters
  SoapInputFilterCollection inputFilters =
      new SoapInputFilterCollection();

  // build collection of output filters
  SoapOutputFilterCollection outputFilters =
      new SoapOutputFilterCollection();

  // add desired output filters
  outputFilters.Add(new TraceOutputFilter());
  outputFilters.Add(new TimestampOutputFilter());

  // Build SOAP message with nothing in it
  SoapEnvelope env = new SoapEnvelope();
  XmlElement body = env.CreateBody();
  env.Envelope.AppendChild(body);

  // Print out the original message
  Console.WriteLine("Original message:\n\n{0}\n", env.OuterXml);

  // Create Pipeline to encapsulate filter collections
  Pipeline pipe = new Pipeline(inputFilters, outputFilters);

  // Process message using all output filters in 
  // pipeline's collection
  pipe.ProcessOutputMessage(env);

  // Print out the output filtered message
  Console.WriteLine("Output filtered message:\n\n{0}\n", env.OuterXml);
}

This application builds a SoapInputFilterCollection, which it leaves empty. Then it builds a SoapOutputFilterCollection and adds a TraceOutputFilter object and a TimestampOutputFilter object to it. It uses the two collections to initialize a new Pipeline object. Then it uses the Pipeline to process an empty SoapEnvelope. The output from the program is shown below.

Original message:
 
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body/>
</soap:Envelope>

Output filtered message:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsu:Timestamp
      xmlns:wsu="https://schemas.xmlsoap.org/ws/2002/07/utility">
      <wsu:Created>2002-11-14T23:17:48Z</wsu:Created>
      <wsu:Expires>2002-11-14T23:22:48Z</wsu:Expires>
    </wsu:Timestamp>
  </soap:Header>
  <soap:Body />
</soap:Envelope>

Order of Filter Processing

In the example above, the TimestampOutputFilter adds a timestamp header to a message and the TraceOutputFilter writes the message to a trace file. The question is, does the message written to the trace file include the timestamp header? The answer is yes. At first, this is somewhat counterintuitive. The SoapOutputFilterCollection.Add method appends filters to the end of the collection, so the TraceOutputFilter comes before the TimestampOutputFilter, suggesting that the message will be written to the trace file before the timestamp header is added. It turns out, however, that output filters are executed in reverse order; that is, the last output filter in the collection processes the message first. Let me explain why.

Consider two filters that alter the content of an output message, TimetampOutputFilter and SecurityOutputFilter. If the TimetampOutputFilter is going to add a timestamp header to an outbound message, it needs to do it before the SecurityOutputFilter encrypts the message. So the order of output filter processing is important. Furthermore, when an input message arrives the SecurityInputFilter has to decrypt the message before the TimestampInputFilter can check to see whether the message has expired. So the order of input filter processing is also important and, in general, needs to be the reverse of the order of the corresponding output filters.

The Pipeline class takes all this into account. Here's how it works. The first filter in a filter collection is "closest to the wire". The last filter in a filter collection is "closest to the code". This is true for both output filters and input filters. Input filters are always processed in the order in which they appear in the collection. Output filters are always processed in the reverse order. All of this is encapsulated inside the ProcessInputMessage and ProcessOutputMessage methods. Figure 2 illustrates this architecture.

Figure 2. The order of filter processing in the Pipeline class

The Pipeline class works this way for a very good reason. If the input and output filter collections were both traversed in the same order, you would have to reverse the order of one set of filters manually in your code. By using a reverse traversal in ProcessOutputMessage, the Pipeline class frees you from that. Instead, you typically add corresponding input and output filters to their respective collections in the same order, and let Pipeline do the rest.

The Default Pipeline

In most applications, you are likely to want all your Pipeline objects to use the same input and output filters. To facilitate this, WSE provides support for default Pipeline configuration. The Microsoft.Web.Services.Configuration.WebServicesConfiguration class provides a static FilterConfiguration property that maintains a pair of filter collections, exposed as the InputFilters and OutputFilters properties, respectively. When the Pipeline class is instantiated using the default constructor, references to the default filters are copied into the new object's filter collections. You can modify the default Pipeline configuration on a per-AppDomain basis.

The default input and output filter collections are pre-configured with the built-in WSE filters. The configuration is shown in Table 2.

Table 2. Pre-configured default Pipeline configuration

Index Input Filter Output Filter
0 SecurityInputFilter SecurityOutputFilter
1 TimestampInputFilter TimestampOutputFilter
2 ReferralInputFilter ReferralOutputFilter
3 RoutingInputFilter RoutingOutputFilter

Remember that the filters at lower indices run "closer to the wire" and filters with higher indices run "closer to the code". If the tracing feature in WSE is enabled, TraceInputFilter and TraceOutputFilter are inserted at the front of the default filter collections, that is, at position 0. This makes sense because the goal of the tracing filters is to record going out to or coming in from the wire.

You can modify an AppDomain's default filter collections if you want to. For instance, if your application wants to use WS-Security, but not WS-Routing or WS-Referral, you could remove the routing and referral filters. In general, removing filters you don't need can improve performance by reducing the number of objects that have to examine each SOAP message.

Here is the method that removes the routing and referral headers from the default input and output filters.

public static void ReconfigureDefaultPipeline()
{
  // retrieve default input filter collection
  SoapInputFilterCollection defaultInputFilters =
    WebServicesConfiguration.FilterConfiguration.InputFilters;

  // remove routing and referral input filters
  defaultInputFilters.Remove(typeof(RoutingInputFilter));
  defaultInputFilters.Remove(typeof(ReferralInputFilter));

  // retrieve default output filter collection
  SoapOutputFilterCollection defaultOutputFilters =
    WebServicesConfiguration.FilterConfiguraiton.OutputFilters;

  // remove routing and referral output filters
  defaultOutputFilters.Remove(typeof(RoutingOutputFilter));
  defaultOutputFilters.Remove(typeof(ReferralOutputFilter));
}

It is important to note that modifications to the default filter collections have no effect on existing Pipeline objects. Only new Pipeline objects created after the default collections have been changed will reflect the changes. For instance, in the code below, the two Pipeline objects user different filters.

static void Main(string[] args)
{
  // first pipeline has all filters
  Pipeline pipe1 = new Pipeline();
  
  // modify default filter collections,
  // remove routing and referral filters
  ReconfigureDefaultPipeline();

  // second pipeline has security and
  // timestamp filters, but not
  // routing and referral filters
  Pipeline pipe2 = new Pipeline(); 

  ... // use Pipelines
}

If you are using WSE with ASP.NET Web services, you can use a Global.asax file to modify a service's default filter collections when the application starts. Here's an example:

<%@ import namespace="Microsoft.Web.Services" %>
<%@ import namespace="Microsoft.Web.Services.Configuration" %>
<%@ import namespace="Microsoft.Web.Services.Routing" %>
<%@ import namespace="Microsoft.Web.Services.Referral" %>

<script runat="server" language="C#" >

public void Application_OnStart()
{
    SoapInputFilterCollection defaultInputFilters =
        WebServicesConfiguration.FilterConfiguration.InputFilters;
        
    defaultInputFilters.Remove(typeof(RoutingInputFilter));
    defaultInputFilters.Remove(typeof(ReferralInputFilter));    

    SoapOutputFilterCollection defaultOutputFilters =
        WebServicesConfiguration.FilterConfiguration.OutputFilters;
        
    defaultOutputFilters.Remove(typeof(RoutingOutputFilter));
    defaultOutputFilters.Remove(typeof(ReferralOutputFilter));    
}

</script>

Custom Filters

The WSE filter architecture is extensible. You may have noticed that the TimestampOutputFilter class extends Microsoft.Web.Services.SoapOutputFilter and the TimestampInputFilter class extends Microsoft.Web.Services.SoapInputFilter. SoapOutputFilter and SoapInputFilter are both abstract base classes. If you want to write your own filter, all you need to do is derive from the appropriate base class and override the abstract ProcessMessage method.

There are lots of possible uses for custom filters. Here is a custom output filter that checks to see whether an output message generated by a Web service contains a SOAP Fault element. If it does, it writes the contents of the fault to the Windows event log. This output filter does not modify a message in any way.

public class SoapFaultOutputFilter : SoapOutputFilter
{
  public override void ProcessMessage(SoapEnvelope envelope)
  {
    // use XPath to look for fault in message
    XmlNamespaceManager nsMgr =
              new XmlNamespaceManager(envelope.NameTable);
    nsMgr.AddNamespace("soap",
             "https://schemas.xmlsoap.org/soap/envelope/");
    XmlElement fault = (XmlElement)
      envelope.Body.SelectSingleNode("soap:Fault", nsMgr);

    // if there is a fault...
    if (fault != null)
    {
      // get fault children
      XmlNode faultcode = fault.SelectSingleNode("faultcode");
      XmlNode faultstring = fault.SelectSingleNode("faultstring");
      XmlNode faultactor = fault.SelectSingleNode("faultactor");
      XmlNode detail = fault.SelectSingleNode("detail");

      // format fault children into string
      string msg = string.Format("{0}\n{1}\n{2}\n\n{3}",
            ((faultcode != null) ? faultcode.InnerText : ""),
            ((faultstring != null) ? faultstring.InnerText : ""),
            ((faultactor != null) ? faultactor.InnerText : ""),
            ((detail != null) ? detail.OuterXml : ""));

      // write raw fault body to a byte array
      MemoryStream stm = new MemoryStream();
      StreamWriter sw = new StreamWriter(stm);
      sw.Write(fault.OuterXml);
      sw.Flush();
      byte[] buf = stm.ToArray();
      
      // write fault data to event log  
      WriteToEventLog(msg, "WSE", buf);

      sw.Close();
    }
  }

  private void WriteToEventLog(string msg, string src, byte[] buf)
  {
    EventLog eventLog = null;
    try
    {
      // write fault to event log, creating if necessary
      eventLog = new EventLog();
      eventLog.Log = "WSEFaultLog";
      eventLog.Source = src;
      eventLog.WriteEntry(msg, EventLogEntryType.Error, 0, 0, buf);
    }
    finally
    {
      if (eventLog != null) eventLog.Dispose();
    }
  }
}

The SoapFaultOutputFilter.ProcessMessage method uses XPath to probe into the SoapEnvelope passed in as an argument. It looks for a SOAP Fault element. If it finds one, it extracts information about the fault, formats it as a string, and passes it to the WriteToEventLog method. WriteToEventLog uses the System.Diagnostics.EventLog class to write the fault information to a custom event log called "WSEFaultLog", creating it if necessary. It is important to note that this filter will only see SOAP Faults generated by a service itself. If the SecurityInputFilter rejects a message sent from a client because it doesn't have an appropriate signature, this filter won't see it.

The SoapFaultOutputFilter was designed for any Web service that wants to log the fact that it is sending a SOAP Fault to a client. It would be easy to implement a similar input filter that would log the fact that a client has received a SOAP Fault from a service. It is worth noting, however, that no such implementation is required. WSE doesn't mandate that each output filter have a matching input filter, or vice versa. Many tasks, like logging errors or validating the contents of a message's body against a schema, can be accomplished asymmetrically, that is, just for input messages or just for output messages, but not both.

Using SoapContext to Communicate with a Custom Filter

The built-in WSE filters communicate with the rest of an application via the properties of the SoapContext object attached to a SoapEnvelope. You can use the same technique for custom filters. The SoapContext class contains a System.Collections.Hashtable object for storing arbitrary data. The Hashtable is exposed indirectly through the methods shown below.

public class SoapContext
{
  public void Add(string key, object value);
  public void Clear();
  public bool Contains(string key);
  public IDictionaryEnumerator GetEnumerator();
  public void Remove(string key);
  ... // other properties and methods omitted
}

The SoapFaultOutputFilter could be modified to use SoapContext to allow an application to specify what source string to use when logging an error. In the implementation SoapFaultOutputFilter.ProcessMessage above, the source string was hard-coded to "WSE":

      // write fault data to event log  
      WriteToEventLog(msg, "WSE", buf);

Here's a modified version of the code that pulls the source string from SoapContext if it is present. The value is identified by the key "FaultSource":

      // write fault data to event log
      string src = "WSE";
      if (envelope.Context.Contains("FaultSource"))
        src = envelope.Context["FaultSource"];
      WriteToEventLog(msg, src, buf);

For more sophisticated custom filters, you could hang an entire additional object model off of SoapContext using this technique.

Configuring a Custom Filter

Once you've implemented a custom filter, you have to configure the Pipeline to use it. You can do this declaratively by adding an entry to your application's .config file, as shown below.

<configuration>
  <microsoft.web.services>
    <filters>
      <!-- note that you can add custom filters asymmetrically,
           that is, just for output or just for input -->
      <output>
        <add type="SoapFaultOutputFilter,WSEFilters" />        
      </output>
    </filters>
  </microsoft.web.services>
</configuration>

Custom filters that are configured this way are automatically added to the end of the default filter collections, so they always run "closer to the code" than the built-in WSE filters. You can also configure a custom filter manually, by modifying the default filter collections for your AppDomain, as in:

WebServicesConfiguration.FilterConfiguration.OutputFilters.Add(
  new SoapFaultOutputFilter();

You have to use the latter approach if you want to do anything other than append your custom filter to the end of the appropriate default filter collection.

What About DIME?

The only significant WSE feature that is not implemented with output and input filters is support for DIME and WS-Attachments. The DIME specification defines a binary format for messages. The WS-Attachments specification describes how to use DIME to attach binary data to a SOAP message. The filter chains used by the Pipeline class perform "in situ" transformations of individual SoapEnvelope objects. Mapping SOAP messages to and from DIME records requires a transformation that produces a fundamentally different format, either from SOAP message to binary stream or vice versa. This is simply a reflection of the fact that, unlike the other Web services protocols, DIME is not actually SOAP-based. In short, when you use DIME you are going beyond SOAP and the WSE filter model doesn't support this type of transformation, so a separate mechanism is required.

The classes in the Microsoft.Web.Services.Dime namespace implement the WSE DIME functionality. The DimeWriter and DimeReader classes provide stream-based IO for DIME messages. Both classes use DimeRecord objects to represent the individual records in a DIME message. After calling Pipeline.ProcessOutputMessage, you can use DimeWriter to transform an outbound SOAP message into a DIME message. Similarly, you can use DimeReader to transform an inbound DIME message into a SOAP message before calling Pipeline.ProcessInputMessage.

Most Web services developers think of DIME, in conjunction with WS-Attachments, as a way to send binary data along with a SOAP message. But DIME has another more interesting use. If you are going to send a SOAP message over a stream-based protocol, you need some way to specify how big it is so that a receiver knows when it ends and the next message begins. When you send SOAP messages over HTTP, you can use the Content-Length header for this purpose. If you are sending SOAP messages some other way, you can use DIME. Here is a function that converts a SOAP message to a DIME message with a single record.

        
public void EnvelopeToDIMEStream(SoapEnvelope env, Stream stm)
{
  // create writer for DIME message
  DimeWriter dw = new DimeWriter(stm);

  // generate new record id
  string id = string.Format("uuid:{0}", guid.NewGuid().ToString());

  // create new record, indicating content will
  // be SOAP envelope and that length should be
  // calculated (-1)
  DimeRecord rec = dw.LastRecord(id,
         "https://schemas.xmlsoap.org/soap/envelope/",
         TypeFormatEnum.AbsoluteUri, -1);

  // write SOAP message to DIME record
  env.Save(rec.BodyStream);

  // cleanup
  dw.Close();
}

And here is the corresponding function that maps back the other way.

public DIMEStreamToEnvelope(Stream stm, SoapEnvelope env)
{
  // create reader for DIME message
  DimeReader dr = new DimeReader(stm);

  // read record containing SOAP message
  DimeRecord rec = dr.ReadRecord();

  // read SOAP message from DIME record
  env.Load(rec.BodyStream);

  // make sure there are no more records
  Debug.Assert(rec.MessageEnd);
}

If you are using DIME as a way to attach arbitrary data to SOAP messages, your reading and writing code becomes more complex. The key thing WSE does to help is it provides a place to store attachment data as part of a SoapEnvelope object's SoapContext. Specifically, the SoapContext.Attachments property, of type DimeAttachmentCollection, can hold DimeAttachment objects for you. Each DimeAttachment corresponds to a DIME record and contains an id, a type identifier, a chunk size and a binary data stream.

Conclusion

Web Services Enhancements (WSE) functionality is implemented (primarily) using filters that process inbound and outbound messages. You can use filters individually or in pipelines, and you can control the default configuration of pipelines in the process. You can also build custom filters that add whatever new functionality you require. In short, the creators of WSE have provided an incredibly flexible, extensible architecture. Whether you use WSE in conjunction with ASP.NET Web services or on its own, having a deep knowledge of how filters and pipelines work will help you take full advantage of all the power WSE has to offer.

Programming with Web Services Enhancements 1.0 for Microsoft .NET

Understanding DIME and WS-Attachments

Web Services Enhancements 1.0 for Microsoft .NET

WS-Security Authentication and Digital Signatures with Web Services Enhancements