Note Four types of operations exist: one-way, request-response, solicit-response, and notification. The current WSDL specification defines bindings only for the one-way and request-response operation types. The other two can have bindings defined via binding extensions. The latter two are simply the inverse of the first two; the only difference is whether the endpoint in question is on the receiving or sending end of the initial message. HTTP is a two-way protocol, so the one-way operations will work only with MIME (which is not supported by ASP.NET) or with another custom extension.
Binding Section
The <binding> elements link the abstract data format to the concrete protocol used for transmission over an Internet connection. So far, the WSDL document has specified the data type used for various pieces of information, the required messages used for an operation, and the structure ofeach message. With the <binding> element, the WSDL document specifies the low-level communication protocol that you can use to communicate with a web service. It links this to an <operation>
from the <portType> section.
Although we won't go into all the details of SOAP encoding, here's an example that defines how SOAP communication should work with the GetEmployeesCount() method of the EmployeesService:
<binding name="EmployeesServiceSoap" type="s0:EmployeesServiceSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" />
<operation name="GetEmployeesCount">
<soap:operation soapAction="http://www.apress.com/ProASP.NET/GetEmployeesCount"
style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
If your method uses a SOAP header, that information is added as a <header> element. Here's an example with the CreateSession() method from the custom state web service developed earlier in this chapter. The CreateSession() method doesn't require the client to submit a header, but it does return one. As a result, only the output message references the header:
<operation name="CreateSession">
<soap:operation soapAction="http://tempuri.org/CreateSession" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
<soap:header message="s0:CreateSessionSessionHeader" part="SessionHeader" use="literal" />
</output>
</operation>
In addition, if your web service supports SOAP 1.2, you'll find a duplicate <binding> section with similar information:
<binding name="EmployeesServiceSoap12" type="s0:EmployeesServiceSoap">
...
</binding>
Remember, .NET web services support both SOAP 1.1 and SOAP 1.2 by default, but you can change this using configuration files, as shown earlier in this chapter.
Service Section
The <service> section defines the entry points into your web service, as one or more <port> elements. Each <port> provides address information or a URI. Here's an example from the WSDL for the EmployeesService:
<service name="EmployeesService">
<documentation>Retrieve the Northwind Employees</documentation>
<port name="EmployeesServiceSoap" binding="s0:EmployeesServiceSoap">
<soap:address location="http://localhost/WebService1/EmployeesService.asmx" />
</port>
</service>
The <service> section also includes a <documentation> element with the Description property of the WebService attribute, if it's set.
Implementing an Existing Contract
Since web services first appeared, a fair bit of controversy has existed about the right way to develop them. Some developers argue that the best approach is to use platforms such as .NET that abstract away the underlying details. They want to work with a higher-level framework of remote procedure calls. But XML gurus argue that you should look at the whole system in terms of XML message passing. They believe the first step in any web service application should be to develop a WSDL contract by hand.
As with many controversies, the ultimate solution probably lies somewhere in between. Application developers will probably never write WSDL contracts by hand-it's just too tedious and error-prone. On the other hand, developers who need to use web services in broader cross-platform scenarios will need to pay attention to the underlying XML representation of their messages and use techniques such as XML serialization attributes (described in the next section) to make sure they adhere to the right schema.
.NET 1.x was unashamedly oriented toward remote procedure calls. It significantly restricted the ability of developers to get underneath the web service facade and tinker with the low-level details. .NET 2.0 addresses this limitation with increased support for XML-centric approaches. One example is contract-first development.
In the web service scenarios you've seen so far, the web service code is created first. ASP.NET generates a matching WSDL document on demand. However, using .NET 2.0, you can approach the problem from the other end. That means you can take an existing WSDL document and feed it into the wsdl.exe command-line utility to create a basic web service skeleton. All you need is the new /serverInterface command-line switch
For example, you could use the following command line to create a class definition for the EmployeesService:
wsdl /serverInterface http://localhost/EmployeesService/EmployeesService.asmx?WSDL
You'll end up with an interface like this:
public interface IEmployeesServiceSoap
{
[WebMethod()]
DataSet GetEmployees();
}
You can then implement the interface in another class to add the web service code for the GetEmployees() method. By starting with the WSDL control first, you ensure that your web service implementation matches it exactly.
Note The interface isn't quite this simple. To make sure your interface exactly matches the WSDL, .NET adds a number of attributes that specifically set details such as namespaces, SOAP encoding, and XML element names. This clutters the interface, but the basic structure is as shown here.
You could also use this trick with a third-party web service. For example, you might want to create your own version of the stock-picking web service on XMethods. You want to ensure that clients can call your web method without needing to get a new WSDL document or be recompiled. To ensure this, you can generate and implement an exact interface match:
wsdl /serverInterface
http://services.xmethods.net/soap/urn:xmethods-delayed-quotes.wsdl
Of course, for your web service to really be compatible, your code needs to follow certain assumptions that aren't set out in the WSDL document. These details might include how you parse strings, deal with invalid data, handle exceptions, and so on.
Contract-first development is unlikely to replace the simpler class-first model. However, it's a useful feature for developers who need to adhere to existing WSDL contracts, particularly in a crossplatform scenario.
Customizing SOAP Messages
In many cases, you don't need to worry about the SOAP serialization details. You'll be happy enough to create and consume web services using the infrastructure that .NET provides. However, in other cases you may need to extend your web services to use custom types or serialize your types to a specific XML format (for cross-platform compatibility). In the following sections, you'll see how you can control these details.
Serializing Complex Data Types
As you learned in the previous chapter, the SOAP specification supports all the data types defined by the XML Schema standard. These are considered simple types. Additionally, SOAP supports complex types, which are structures built out of an arrangement of simple types. You can use complex types for a web method return value or as a parameter. However, if a web method requires complex type parameters, you can interact with it only using SOAP. The simpler HTTP GET and HTTP POST mechanisms won't work, and the browser test page won't allow you to invoke the web method. You've already used one example of a complex type: the DataSet. When you call the GetEmployees() method in the EmployeesService, .NET returns an XML document that describes the schema of the DataSet and its contents. Here's a partial listening of the SOAP response message:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
<soap:Body>
<GetEmployeesResponse xmlns="http://www.apress.com/ProASP.NET/">
<GetEmployeesResult>
<xs:schema id="NewDataSet" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<!-- Schema omitted. -->
</xs:schema>
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<EmployeesDataSet xmlns="">
<Employees diffgr:id="Employees1" msdata:rowOrder="0">
<EmployeeID>1</EmployeeID>
<LastName>Davolio</LastName>
<FirstName>Nancy</FirstName>
<Title>Sales Representative</Title>
<TitleOfCourtesy>Ms.</TitleOfCourtesy>
<HomePhone>(206) 555-9857</HomePhone>
</Employees>
<Employees diffgr:id="Employees2" msdata:rowOrder="1">
<EmployeeID>2</EmployeeID>
<LastName>Fuller</LastName>
<FirstName>Andrew</FirstName>
<Title>Vice President, Sales</Title>
<TitleOfCourtesy>Dr.</TitleOfCourtesy>
<HomePhone>(206) 555-9482</HomePhone>
</Employees>
...
</EmployeesDataSet></diffgr:diffgram>
</GetEmployeesResult>
</GetEmployeesResponse>
</soap:Body>
</soap:Envelope>
You can also use your own custom classes with .NET web services. In this case, when you build the proxy, a copy of the custom class will automatically be added to the client (in the appropriate language of the client).
The process of converting objects to XML is known as serialization, and the process of reconstructing the objects from XML is know as deserialization. The component that performs the serialization is the System.Xml.Serialization.XmlSerializer class. You shouldn't confuse this class with the serialization classes you learned about in Chapter 13, such as the BinaryFormatter and SoapFormatter. These classes perform .NET-specific serialization that works with proprietary .NET objects, as long as they are marked with the Serializable attribute. Unlike the BinaryFormatter and SoapFormatter, the XmlSerializer works with any class, but it's much more limited than the Binary- Formatter and SoapFormatter and can extract only public data.
To use the XmlSerializer and send your custom objects to and from a web service, you need to be aware of a few restrictions:
- Any code you include is ignored in the client: This means the client's copy of the custom class won't include methods, constructor logic, or property procedure logic. Instead, these details will be stripped out automatically.
- Your class must have a default zero-argument constructor: This allows .NET to create a new instance of this object when it deserializes a SOAP message that contains the corresponding data.
- Read-only properties are not serialized: In other words, if a property has only a get accessor and not a set accessor, it cannot be serialized. Similarly, private properties and private membervariables are ignored.
Clearly, the need to serialize a class to a piece of cross-platform XML imposes some strict limitations. If you use custom classes in a web service, it's best to think of them as simple data containers, rather than true participants in object-oriented design.
Creating a Custom Class
To see the XmlSerializer in action, you need to create a custom class and a web method that uses it. In the next example, we'll use the database component first developed in Chapter 8. This database component doesn't use the disconnected DataSet objects. Instead, it returns the results of a query using the custom EmployeeDetails class.
Here's what the EmployeeDetails class looks like currently, without any web service-related enhancements:
public class EmployeeDetails
{
private int employeeID;
public int EmployeeID
{
get { return employeeID; }
set { employeeID = value; }
}
private string firstName;
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
private string lastName;
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
private string titleOfCourtesy;
public string TitleOfCourtesy
{
get { return titleOfCourtesy; }
set { titleOfCourtesy = value; }
}
public EmployeeDetails(int employeeID, string firstName, string lastName,
string titleOfCourtesy)
{
this.employeeID = employeeID;
this.firstName = firstName;
this.lastName = lastName;
this.titleOfCourtesy = titleOfCourtesy;
}
}
The EmployeeDetails class uses property procedures instead of public member variables. However, you can still use it because the XmlSerializer will perform the conversion automatically. The EmployeeDetails class doesn't have a default zero-parameter constructor, so before you can use it in a web method you need to add one, as shown here:
public EmployeeDetails(){}
Now the EmployeeDetails class is ready for a web service scenario. To try it, you can create a web method that returns an array of EmployeeDetail objects. The next example shows one such method-a GetEmployees() web method that calls the EmployeeDB.GetEmployees() method in the database component. (For the full code for this method, you can refer to Chapter 8 or consult the downloadable code.)
Here's the web method you need:
[WebMethod()]
public EmployeeDetails[] GetEmployees()
{
EmployeeDB db = new EmployeeDB();
return db.GetEmployees();
}
Generating the Proxy
When you generate the proxy (either using wsdl.exe or adding a web reference), you'll end up with two classes. The first class is the proxy class used to communicate with the web service. The second class is the definition for EmployeeDetails.
It's important to understand that the client's version of EmployeeDetails doesn't match the server-side version. In fact, the client doesn't even have the ability to see the full code of the serverside EmployeeDetails class. Instead, the client reads the WSDL document, which contains the XML schema for the EmployeeDetails class. The schema simply lists all the public properties and fields (without distinguishing between the two) and their data types.
When the client builds a proxy class, .NET uses this WSDL information to generate a client-sideEmployeeDetails class. For every public property or field in the server-side definition of Employee-Details, .NET adds a matching public property to the client-side EmployeeDetails class.
Here's the code that's generated for the client-side EmployeeDetails class:
public partial class EmployeeDetails
{
private int employeeIDField;
private string firstNameField;
private string lastNameField;
private string titleOfCourtesyField;
public int EmployeeID
{
get { return this.employeeIDField; }
set { this.employeeIDField = value; }
}
public string FirstName
{
get { return this.firstNameField; }
set { this.firstNameField = value; }
}
public string LastName
{
get { return this.lastNameField; }
set { this.lastNameField = value; }
}
public string TitleOfCourtesy
{
get { return this.titleOfCourtesyField; }
set { this.titleOfCourtesyField = value; }
}
}
In this example, the client-side version is quite similar to the server-side version, because the server-side version didn't include much code. The only real difference (other than the renaming of private fields) is the missing nondefault constructor. As a general rule, the client-side version doesn't preserve nondefault constructors, any code in property procedures or constructors, any methods, or any private members.
Note The client-side version of a data class always uses property procedures, even if the original server-side version used public member variables. That gives you the ability to bind collections of client-side EmployeeDetails objects to a grid control. This is a change from .NET 1.x.
Testing the Custom Class Web Service
The next step is to write the code that calls the GetEmployees() method. Because the client now has a definition of the EmployeeDetails class, this step is easy:
EmployeesServiceCustomDataClass proxy = new EmployeesServiceCustomDataClass();
EmployeeDetails[] employees = proxy.GetEmployees();
The response message includes the employee data in the <GetEmployeesResult> element. By default, the XmlSerializer creates a structure of child elements based on the class name (Employee- Details) and the public property or variable names (EmployeeID, FirstName, LastName, TitleOf- Courtesy, and so on). Interestingly, this default structure looks quite a bit like the XML used to model the DataSet, without the schema information.
Here's a somewhat abbreviated example of the response message:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
<soap:Body>
<GetEmployeesResponse xmlns="http://www.apress.com/ProASP.NET/">
<GetEmployeesResult>
<EmployeeDetails>
<EmployeeID>1</EmployeeID>
<FirstName>Nancy</FirstName>
<LastName>Davolio</LastName>
<TitleOfCourtesy>Ms.</TitleOfCourtesy>
</EmployeeDetails>
<EmployeeDetails>
<EmployeeID>2</EmployeeID>
<FirstName>Andrew</FirstName>
<LastName>Fuller</LastName>
<TitleOfCourtesy>Dr.</TitleOfCourtesy>
</EmployeeDetails>
</GetEmployeesResult>
...
</GetEmployeesResponse>
</soap:Body>
</soap:Envelope>
When the client receives this message, the XML response is converted into an array of EmployeeDetails objects, using the client-side definition of the EmployeeDetails class. Customizing XML Serialization with Attributes Sometimes, you may want to customize the XML representation of a custom class. This approach is most useful in cross-platform programming scenarios when a client expects XML in a certain form.
For example, you might have an existing schema that expects EmployeeDetails to use an EmployeeID attribute instead of a nested <EmployeeID> tag. .NET provides an easy way to apply these rules, using attributes. The basic idea is that you apply attributes to your data classes (such as Employee- Details). When the XmlSerializer creates a SOAP message, it reads these attributes and uses them to tailor the XML payload it generates.
The System.Xml.Serialization namespace contains a number of attributes that can be used to control the shape of the XML. Two sets of attributes exist: one where the attributes are named XmlXxx and another where the attributes are named SoapXxx. Which attributes you use depends on how the parameters are encoded.
As discussed earlier in this chapter, two types of SOAP serialization exist: literal and SOAP section 5 encoding. The XmlXxx attributes apply when you use literal style parameters. As a result, they apply in the following cases:
- When you use a web service with the default encoding. (In other words, you haven't changed the encoding by adding any attributes.)
- When you use the HTTP GET or HTTP POST protocols to communicate with a web service.
- When you use the SoapDocumentService or SoapDocumentMethod attribute with the Use property set to SoapBindingUse.Literal.
- When you use the XmlSerializer on its own (outside a web service).
The SoapXxx attributes apply when you use encoded-style parameters. That occurs in the following cases:
- When you use the SoapRpcService or SoapRpcMethod attributes
- When you use the SoapDocumentService or SoapDocumentMethod attribute with the Use property set to SoapBindingUse.Encoded
A class member may have both the SoapXxx and the XmlXxx attributes applied at the same time. Which one is used depends on the type of serialization being performed. Table 32-1 lists most of the available attributes. Most of the attributes contain a number of properties. Some properties are common to most attributes, such as the Namespace property (used to indicate the namespace of the serialized XML) and the DataType property (used to indicate a specific XML Schema data type that might not be the one the XmlSerializer would choose by default). For a complete reference that describes all the attributes and their properties, refer to the MSDN Help.
Table 32-1. Attributes to Control XML Serialization
Xml Attribute |
SOAP Attribute |
Description |
XmlAttribute |
SoapAttribute |
Used to make fields or properties into XML attributes instead of elements. |
XmlElement |
SoapElement |
Used to name the XML elements. |
XmlArray |
|
Used to name arrays. |
XmlIgnore |
SoapIgnore |
Used to prevent fields or properties from being serialized. |
XmlInclude |
SoapInclude |
Used in inheritance scenarios. For example, you may have a property or field that's typed as some base class but may actually reference some derived class. In this case, you can use XmlInclude to specify all the derived class types that you may use. |
XmlRoot |
|
Used to name the top-level element. |
XmlText |
|
Used to serialize fields directly in the XML text without elements. |
XmlEnum |
SoapEnum |
Used to give the members of an enumeration a name different from the name used in the enumeration. |
XmlType |
SoapType |
Used to control the name of types in the WSDL file. |
To see how SOAP serialization works, you can apply these attributes to the EmployeeDetails. For example, consider the following modified class declaration that uses several serialization attributes:
public class EmployeeDetails
{
[XmlAttribute("id")]
public int EmployeeID
{
get { return employeeID; }
set { employeeID = value; }
}
[XmlElement("First")]
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
[XmlElement("Last")]
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
[XmlIgnore()]
public string TitleOfCourtesy
{
get { return titleOfCourtesy; }
set { titleOfCourtesy = value; }
}
// (Constructors and private data omitted.)
}
Here's what a serialized EmployeeDetails will look like in the SOAP message:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
<soap:Body>
<GetEmployeesResponse xmlns="http://www.apress.com/ProASP.NET/">
<GetEmployeesResult>
<EmployeeDetails id="1">
<First>Nancy</First>
<Last>Davolio</Last>
</EmployeeDetails>
<EmployeeDetails id="2">
<First>Andrew</First>
<Last>Fuller</Last>
</EmployeeDetails>
...
</GetEmployeesResult>
</GetEmployeesResponse>
</soap:Body>
</soap:Envelope>
Tip If you want to experiment with different serialization attributes, you can also use the XmlSerializer class directly. Just create an instance of the XmlSerializer and pass the type of the object you want to serialize as a constructor parameter. You can then use the Serialize() method to convert the object to XML and write the data to a stream or a TextWriter object. You can use Deserialize() to read the XML data from a stream or TextReader and re-create the original object. You can also use a command-line tool called xsd.exe that's included with the .NET Framework to generate C# class definitions based on XML schema documents. The class declaration will automatically include the appropriate serialization attributes.
This example has only one limitation. Although you can control how the EmployeeDetails object is serialized, you can't use the same attributes to shape the element that wraps the list of employees. To take this step, you have two options. You could create a custom collection class and apply the XML serialization attributes to that class. Or, if you want to continue using an ordinary array, you must add an XML attribute that applies directly to the return value of the web method, like this:
[return: XmlArray("EmployeeList")]
public EmployeeDetails[] GetEmployees()
{ ... }
Now when you call the web method, you'll get this XML:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
<soap:Body>
<GetEmployeesResponse xmlns="http://www.apress.com/ProASP.NET/">
<EmployeeList>
<EmployeeDetails id="1">
<First>Nancy</First>
<Last>Davolio</Last>
</EmployeeDetails>
<EmployeeDetails id="2">
<First>Andrew</First>
<Last>Fuller</Last>
</EmployeeDetails>
...
</EmployeeList>
</GetEmployeesResponse>
</soap:Body>
</soap:Envelope>
You can do a fair bit more to configure the details. For example, you can insert XML serialization attributes immediately before your parameters to change the required XML of the incoming request message. You can also use the SoapDocument attribute (discussed earlier) to change the name and namespace of the XML element that wraps the return value of your function (in this
example, it's named <GetEmployeesReponse>).
Type Sharing
In .NET 1.x, you could run into headaches if more than one web service used the same custom class. For example, you might call the Store.GetOrder() method from one web service to get an Order object and then send that Order object to the Shipping.TrackOrder() method from another web service. The problem is that when you add a reference to both the Store and Shipping web services, you end up with two copies of the Order object data class, in two different namespaces. And even though these class definitions are equivalent, you still can't use them interchangeably. That means if you receive a version of an object from one web service, you can't pass it along to another web service.
.NET 2.0 handles this problem with a new type sharing feature. With type sharing, you can generate one client-side copy of a data class and use it with all compatible web services. To be considered compatible, the custom data class must meet these requirements:
- It must have the same XML representation. In other words, the XML schema of the type must be identical.
- It must have the same XML namespace. Different namespaces indicate different documents. Many other details aren't important. For example, these factors have no influence on your ability to perform type sharing:
- The location of the web service
- The language of the web service
- The name of the class or properties (as long as you apply the XML serialization attributes to make sure the serialized form matches)
For example, the following server-side Employee class looks a fair bit different from the EmployeeDetails class shown in the previous section, but the serialized form is identical:
[XmlElement("EmployeeDetails")]
public class Employee
{
[XmlAttribute("id")]
public int ID;
[XmlElement("First")]
public string FirstName;
[XmlElement("Last")]
public string LastName;
}
This class matches the first requirement (identical XML schema), but it may not match the second (same namespace). You have two ways to make sure the class is in the same namespace. Your first option is to use the same namespace in the WebService attribute for both web services:
[WebService(Namespace="http://www.apress.com/ProASP.NET/")
public class EmployeesServiceCompatible : System.Web.Services.WebService
{ ... }
Usually, this isn't the approach you want. It implies that both web services are the same. A better choice is to use a unique namespace to identify shared XML data structures. You can do this by applying the XmlRoot or XmlElement attribute to the EmployeeDetails and Employee classes, as shown here:
[XmlElement("EmployeeDetails",
Namespace="http://www.apress.com/ProASP.NET/EmployeeDetails")]
public class Employee
{ ... }
Now, the serialized XML in the response message looks like this:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" >
<soap:Body>
<GetEmployeesResponse xmlns="http://www.apress.com/ProASP.NET/">
<GetEmployeesResult>
<EmployeeDetails id="1" xmlns="http://www.apress.com/ProASP.NET/EmployeeDetails">
<First>Nancy</First>
<Last>Davolio</Last>
</EmployeeDetails>
<EmployeeDetails id="2" xmlns="http://www.apress.com/ProASP.NET/EmployeeDetails">
<First>Andrew</First>
<Last>Fuller</Last>
</EmployeeDetails>
...
</GetEmployeesResponse>
</soap:Body>
</soap:Envelope>
Assuming you've satisfied these two requirements, you're ready to generate the shared type. Currently, this is possible only by using the wsdl.exe command-line utility with the /sharetypes switch. You must also supply the address of all the web services that use the same type. Here's an example:
wsdl /sharetypes
http://localhost/EmployeesService2/EmployeesServiceCompatible.asmx?WSDL
http://localhost/EmployeesService2/EmployeesService.asmx?WSDL
If the types aren't identical for some reason, you won't be warned about the problem. Instead, your proxy class will contain multiple versions of the data class, such as EmployeeDetails, Employee- Details1, and so on.
Customizing XML Serialization with IXmlSerializable
The XML serialization attributes work well when you can take advantage of a one-to-one mapping between properties and XML elements or attributes. However, in some scenarios developers need more flexibility to create an XML representation of a type that fits a specific schema. For example, you might need to change the representation of your data types, control the order of elements, or add out-of-band information (such as comments or the date the document was serialized). In other cases, it may be technically possible to use the XML serialization attributes, but it may involve creating an unreasonably awkward class model.
Fortunately, .NET 2.0 provides an IXmlSerializable interface that you can implement to get complete control over your XML. The IXmlSerializable attribute has existed in .NET since version 1.0. However, it was used as a proprietary way to customize the serialization of the .NET DataSet, and it wasn't made available for general use. Now it's fully supported. IXmlSerializable mandates the three methods listed in Table 32-2.
Table 32-2. IXmlSerializable Methods
Method |
Description |
WriteXml() |
In this method you write the XML representation of an instance of your object using an XmlWriter. You need this method in your web service in order for your web service to serialize an object and send it as a return value. |
ReadXml() |
In this method you read the XML from an XmlReader and generate the corresponding object. It's quite possible you won't need this method (in which case it's safe to throw a NotImplementedException. However, you will need it if you have to deserialize an object that your web service is accepting as an input parameter or if you decide to deploy this custom class to the client. |
GetSchema() |
This method is deprecated, and you should return null. If you want the ability to generate the XML schema for your class (which will be incorporated in the WSDL document), you must use the XmlSchemaProvider attribute instead. The XmlSchemaProvider names the method in your class that returns the XML schema document (XSD). |
Chapter 12 discussed the XmlReader and XmlWriter classes in detail. Using them is quite straightforward. Here's an example of a custom class that handles its own XML generation:
public class EmployeeDetailsCustom : IXmlSerializable
{
public int ID;
public string FirstName;
public string LastName;
const string ns = "http://www.apress.com/ProASP.NET/CustomEmployeeDetails";
void IXmlSerializable.WriteXml(XmlWriter w)
{
w.WriteStartElement("Employee", ns);
w.WriteStartElement("Name", ns);
w.WriteElementString("First", ns, FirstName);
w.WriteElementString("Last", ns, LastName);
w.WriteEndElement();
w.WriteElementString("ID", ns, ID.ToString());
w.WriteEndElement();
}
void IXmlSerializable.ReadXml(XmlReader r)
{
r.MoveToContent();
r.ReadStartElement("Employee");
r.ReadStartElement("Name");
FirstName = r.ReadElementString("First", ns);
LastName = r.ReadElementString("Last", ns);
r.ReadEndElement();
r.MoveToContent();
ID = Int32.Parse(r.ReadElementString("ID", ns));
reader.ReadEndElement();
}
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
// (Constructors omitted.)
}
Tip Make sure you read the full XML document, including the closing element tags in the ReadXml() method. Otherwise, .NET may throw an exception when you attempt to deserialize the XML.
Now, if you create a web method like this:
[WebMethod()]
public EmployeeDetailsCustom GetCustomEmployee()
{
return new EmployeeDetailsCustom(101, "Joe", "Dabiak");
}
here's the XML you'll see:
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCustomEmployeeResponse xmlns="http://www.apress.com/ProASP.NET/">
<GetCustomEmployeeResult>
<Employee xmlns="http://www.apress.com/ProASP.NET/CustomEmployeeDetails">
<Name>
<First>Joe</First>
<Last>Tester</Last>
</Name>
<ID>1</ID>
</Employee>
</GetCustomEmployeeResult>
</GetCustomEmployeeResponse>
</soap:Body>
</soap:Envelope>
Note When using IXmlSerializable, the only serialization attributes that have any effect are the ones you apply to the method and the class declaration. Attributes on individual properties and fields have no effect. However, you could use .NET reflection to check for your own attributes and then use them to tailor the XML markup you generate.
Schemas for Custom Data Types
The only limitation in this example is that the client has no way to determine what XML to expect. If you look at the <types> section of the WSDL document for this example, you'll see that the schema is left wide open with the <any> element. This allows any valid XML content.
<s:element name="GetCustomEmployeeResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="GetCustomEmployeeResult">
<s:complexType>
<s:sequence>
<s:element ref="s:schema" />
<s:any />
</s:sequence>
</s:complexType>
</s:element>
</s:sequence>
</s:complexType>
</s:element>
On the client side, you could deal with the data as an XML fragment, in which case you need to write the XML parsing code. However, a better idea is to supply an XML schema for your custom XML representation.
To do this, you need to add a static method to your class that returns the XML schema document as an XmlQualifiedName object, as shown here:
public static XmlQualifiedName GetSchemaDocument(XmlSchemaSet xs)
{
// Get the path to the schema file.
string schemaPath = HttpContext.Current.Server.MapPath("EmployeeDetails.xsd");
// Retrieve the schema from the file.
XmlSerializer schemaSerializer = new XmlSerializer(typeof(XmlSchema));
XmlSchema s = (XmlSchema)schemaSerializer.Deserialize(new XmlTextReader(schemaPath), null);
xs.XmlResolver = new XmlUrlResolver();
xs.Add(s);
return new XmlQualifiedName("EmployeeDetails", ns);
}
Tip This example retrieves the schema document from a file. For best performance, you would cache this document or construct it programmatically. Now you need to point .NET to the right method using the XmlSchemaProvider attribute:
[XmlSchemaProvider("GetSchemaDocument")]
public class EmployeeDetailsCustom : IXmlSerializable
{ ... }
Now, ASP.NET will call this static method when it generates the WSDL document. It will then add the schema information to the WSDL document. However, remember that when you build a client .NET will generate the data class to match the schema, which means the client-side EmployeeDetails will differ quite a bit from the server-side version. (In this example, the clientside
EmployeeDetails class will have a nested Name class because of the organization of XML elements, which probably isn't what you want.)
So, what can you do if you want the same version of EmployeeDetails on both the client and server ends? You could manually change the generated proxy code class, although this change will be discarded each time you rebuild the proxy. A more ermanent option is to use schema importer extensions, which you'll tackle in the "Schema Importer Extensions" section.
Custom Serialization for Large Data Types
One reason you might use IXmlSerializable is to build web services that send large amounts of data. For example, imagine you want to send a large block of binary data that contains the content from a file. You could use a web service like this:
[WebMethod()]
public byte[] DownloadFile(string fileName)
{ ... }
The problem is that this approach assumes you'll read the entire data of the file into memory at once, as a byte array. If the file is several gigabytes in size, this will cripple the computer. A better solution is to use IXmlSerializable to implement chunking. That way, you can send an arbitrarily large amount of data over the wire by writing it one chunk at a time.
In the following sections, you'll see an example the uses IXmlSerializable to dramatically reduce the overhead of sending a large file.
The Server Side
The first step is to create the signature for your web method. For this strategy to work, the web method needs to return a class that implements IXmlSerializable. This example uses a class named FileData. Additionally, you need to turn off ASP.NET buffering to allow the response to be streamed across the network.
[WebMethod(BufferResponse = false)]
[SoapDocumentMethod(ParameterStyle = SoapParameterStyle.Bare)]
public FileData DownloadFile(string serverFileName)
{ ... }
The most laborious part is implementing the custom serialization in the FileData class. The basic idea is that when you create a FileData object on the server, you'll simply specify the corresponding filename. When the FileData object is serialized and IXmlSerializable.WriteXml() is called, the FileData object will create a FileStream and start sending binary data one block at a time.
Here's the bare skeleton of the FileData class:
[XmlRoot(Namespace = "http://www.apress.com/ProASP.NET/FileData")]
[XmlSchemaProvider("GetSchemaDocument")]
public class FileData : IXmlSerializable
{
// Namespace for serialization.
const string ns = "http://www.apress.com/ProASP.NET/FileData";
// The server-side path.
private string serverFilePath;
// When the FileData is created, make sure the file exists.
// This won't defend against other problems reading the file (like
// insufficient rights, the file is currently locked by another process,
// and so on).
public FileData(string serverFilePath)
{
if (!File.Exists(serverFilePath))
{
throw new FileNotFoundException("Source file not found.");
}
this.serverFilePath = serverFilePath;
}
void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
{ ... }
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
void IXmlSerializable.ReadXml(System.Xml.XmlReader reader)
{
throw new NotImplementedException();
}
public static XmlQualifiedName GetSchemaDocument(XmlSchemaSet xs)
{
// Get the path to the schema file.
string schemaPath = HttpContext.Current.Server.MapPath("FileData.xsd");
// Retrieve the schema from the file.
XmlSerializer schemaSerializer = new XmlSerializer(typeof(XmlSchema));
XmlSchema s = (XmlSchema)schemaSerializer.Deserialize(
new XmlTextReader(schemaPath), null);
xs.XmlResolver = new XmlUrlResolver();
xs.Add(s);
return new XmlQualifiedName("FileData", ns);
}
}
You'll notice that this class supports writing the file data to XML but not reading it. That's because you're looking at the server's version of the code. It sends FileData objects, but doesn't receive them.
In this example, you want to create an XML representation that splits data into separate Base64-encoded chunks. It will look like this:
<FileData xmlns="http://www.apress.com/ProASP.NET/FileData">
<fileName>sampleFile.xls</fileName>
<size>66048</size>
<content>
<chunk>...</chunk>
<chunk>...</chunk>
...
</content>
</FileData>
Here's the WriteXml() implementation that does the job:
void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
{
// Open the file (taking care to allow it to be opened by other threads
// at the same time).
FileStream fs = new FileStream(serverFilePath, FileMode.Open,
FileAccess.Read, FileShare.Read);
// Write filename.
writer.WriteElementString("fileName", ns, Path.GetFileName(serverFilePath));
// Write file size (useful for determining progress.)
long length = fs.Length;
writer.WriteElementString("size", ns, length.ToString());
// Start the file content.
writer.WriteStartElement("content", ns);
// Read a 4 KB buffer and write that (in slightly larger Base64-encoded chunks).
int bufferSize = 4096;
byte[] fileBytes = new byte[bufferSize];
int readBytes = bufferSize;
while (readBytes > 0)
{
readBytes = fs.Read(fileBytes, 0, bufferSize);
writer.WriteStartElement("chunk", ns);
// This method explicitly encodes the data. If you use another method,
// it's possible to add invalid characters to the XML stream.
writer.WriteBase64(fileBytes, 0, readBytes);
writer.WriteEndElement();
writer.Flush();
}
fs.Close();
// End the XML.
writer.WriteEndElement();
}
Now you can complete the web service. The DownloadFile() method shown here looks for a user-specified file in a hard-coded directory. It creates a new FileData object with the full path name and returns it. At this point, the FileData serialization code springs into action to read the file and begin writing it to the response stream.
public class FileService : System.Web.Services.WebService
{
// Only allow downloads in this directory.
string folder = @"c:\Downloads";
[WebMethod(BufferResponse = false)]
[SoapDocumentMethod(ParameterStyle = SoapParameterStyle.Bare)]
public FileData DownloadFile(string serverFileName)
{
// Make sure the user only specified a filename (not a full path).
serverFileName = Path.GetFileName(serverFileName);
// Get the full path using the download directory.
string serverFilePath = Path.Combine(folder, serverFileName);
// Return the file data.
return new FileData(serverFilePath);
}
}
You can try this method using the browser test page and verify that the data is split into chunks by looking at the XML.
The Client Side
On the client side, you need a way to retrieve the data one chunk at a time and write it to a file.To provide this functionality, you need to change the proxy class so that it returns a custom IXmlSerializable type. You'll place the deserialization code in this class.
Tip You can implement both the serialization code and the deserialization code in the same class and distribute that class as a component to the client and the server. However, it's usually better to observe a strict separation between both ends of a web service application. This makes it easier to update the client with new versions.
When you create the proxy class, .NET will try to create a suitable copy of the FileData class. However, it won't succeed. Without the schema information, it will simply try to convert the returned value to a DataSet. Even if you add the schema information, all .NET can do is create a class representation that exposes all the details (the name, size, and content) through separate properties. This class won't have the chunking behavior-instead, it will attempt to load everything into memory at once.
To fix this problem, you need to customize the proxy class by hand. If you're creating a web client, you need to first generate the proxy class with wsdl.exe so you have the code available. Here's the change you need to make:
public FileDataClient DownloadFile(string serverFileName)
{
object[] results = this.Invoke("DownloadFile", new object[] {serverFileName});
return ((FileDataClient)(results[0]));
}
Obviously, modifying the proxy class is a brittle solution, because every time you refresh the proxy class your change will be wiped out. A better choice is to implement a schema importer extension, as described in the next section.
Here's the basic outline of the FileDataClient class:
[XmlRoot(Namespace = "http://www.apress.com/ProASP.NET/FileData")]
public class FileDataClient : IXmlSerializable
{
private string ns = "http://www.apress.com/ProASP.NET/FileData";
// The location to place the downloaded file.
private static string clientFolder;
public static string ClientFolder
{
get { return clientFolder; }
set { clientFolder = value; }
}
void IXmlSerializable.ReadXml(System.Xml.XmlReader reader)
{ ... }
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer)
{
throw new NotImplementedException();
}
}
One important detail is the static property ClientFolder, which keeps track of the location where you want to save all downloaded files. You must set this property before the download begins, because the ReadXml() method uses that information to determine where to create the file. The ClientFolder property must be a static property, because the client doesn't get the chance to create and configure the FileDataClient object it wants to use. Instead, .NET creates a FileDataClient instance automatically and uses it to deserialize the data. By using a static property, the client can set this piece of information before starting the download, as shown here:
FileDataClient.ClientFolder = @"c:\MyFiles";
The deserialization code performs the reverse task of the serialization code-it steps through the chunks and writes them to the new file. Here's the complete code:
void IXmlSerializable.ReadXml(System.Xml.XmlReader reader)
{
if (FileDataClient.ClientFolder == "")
{
throw new InvalidOperationException("No target folder specified.");
}
reader.ReadStartElement();
// Get the original filename.
string fileName = reader.ReadElementString("fileName", ns);
// Get the size (not currently used).
double size = Convert.ToDouble(reader.ReadElementString("size", ns));
// Create the file.
FileStream fs = new FileStream(Path.Combine(ClientFolder, fileName),
FileMode.Create, FileAccess.Write);
// Read the XML and write the file one block at a time.
byte[] fileBytes;
reader.ReadStartElement("content", ns);
double totalRead = 0;
while (true)
{
if (reader.IsStartElement("chunk", ns))
{
string bytesBase64 = reader.ReadElementString();
totalRead += bytesBase64.Length;
fileBytes = Convert.FromBase64String(bytesBase64);
fs.Write(fileBytes, 0, fileBytes.Length);
fs.Flush();
// You could report progress by raising an event here.
Console.WriteLine("Received chunk.");
}
else
{
break;
}
}
fs.Close();
reader.ReadEndElement();
reader.ReadEndElement();
}
Here's a complete console application that uses the FileService:
static void Main()
{
Console.WriteLine("Downloading to c:\\");
FileDataClient.ClientFolder = @"c:\";
Console.WriteLine("Enter the name of the file to download.");
Console.WriteLine("This is a file in the server's download directory.");
Console.WriteLine("The download directory is c:\\temp by default.");
Console.Write("> ");
string file = Console.ReadLine();
FileService proxy = new FileService();
Console.WriteLine();
Console.WriteLine("Starting download.");
proxy.DownloadFile(file);
Console.WriteLine("Download complete.");
}
Figure 32-8 shows the result:

Figure 32-8. Downloading a large file with chunking
You can find this example online, with a few minor changes (for example, the client and server methods for working with the file are combined into one FileData class).
Schema Importer Extensions
One of the key principles of service-oriented design is that the client and the server share contracts, not classes. That level of abstraction allows clients on widely different platforms to interact with the same web service. They send the same serialized XML, but they have the freedom to use different programmatic structures (such as classes) to prepare their messages.
In some cases, you might want to bend these rules to allow your clients to work with rich data types. For example, you might want to take a custom data class, distribute it to both the client and server, and allow them to send or receive instances of this class with a web service. .NET 2.0 makes this possible with a new feature called schema importer extensions.
Tip Think twice about using custom data types. The danger of this approach is that it can easily lead you to develop a proprietary web service. Although your web service will still use XML (which can always be read on any platform), once you start tailoring your XML to fit platform-specific types, it might be prohibitively difficult for other clients to parse that XML or do anything practical with it. For example, most non-.NET clients don't have an easy way to consume the XML generated for the DataSet.
Before developing a schema importer extension, make sure your service is using the XmlSchemaProvider attribute to designate a method that returns schema information. Without the schema information, the proxy generation tool won't have the information it needs to identify your custom data types, so any schema importers you create will be useless.
For the FileData class, the schema is drawn from this schema file:
<xs:schema id="FileData" targetNamespace=http://www.apress.com/ProASP.NET/FileData
elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="FileData" >
<xs:sequence>
<xs:element name="fileName" type="xs:string" />
<xs:element name="size" type="xs:int" />
<xs:element name="content" >
<xs:complexType >
<xs:sequence>
<xs:element name="chunk" type="xs:base64Binary" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>
Now you're ready to develop the schema importer that allows the client to recognize this data type. Using a schema importer involves two steps: creating the extension and then registering it.
To create the extension, you need to create a new class library component (DLL assembly). In this assembly, add a class that derives from SchemaImporterExtension. When the proxy generator comes across a complex type (as it generates a proxy class), it calls the ImportSchemaType() method of every schema importer extension defined in the machine.config file. Each schema importer can check the namespace and schema of the type and then decide to handle it by mapping the XML type to a known .NET type.
Here's an example with a FileDataSchemaImporter that configures the proxy to use the FileDataClient class:
public class FileDataSchemaImporter : SchemaImporterExtension
{
public override string ImportSchemaType(string name, string ns,
XmlSchemaObject context, XmlSchemas schemas, XmlSchemaImporter importer,
CodeCompileUnit compileUnit, CodeNamespace mainNamespace,
CodeGenerationOptions options, CodeDomProvider codeProvider)
{
if (name.Equals("FileData") &&
ns.Equals("http://www.apress.com/ProASP.NET/FileData"))
{
mainNamespace.Imports.Add(new CodeNamespaceImport("FileDataComponent"));
return "FileDataClient";
}
else
{
// Chose not to handle the type.
return null;
}
}
}
This is an extremely simple schema importer. It does two things:
- It instructs the proxy class to use the class named FileDataClient for this type. That means the proxy class will use your existing class and refrain from generating a client-side FileData class automatically (the standard behavior).
- It instructs the proxy class generator to add a namespace import for the FileDataComponent namespace. It's still up to you to make sure the assembly with the FileDataComponent.File- Data class is available in your project.
Once you've created the schema importer, you need to install it in the global assembly cache. Give it a strong name (use the Signing tab in the project properties) and then drag and drop it into the c:\[WinDir]\Assembly directory, or use the gacutil.exe command-line utility. Once your schema importer is safely installed in the cache, use Windows Explorer to find out its public key token. Armed with that information, you can register your schema importer in the machine.config file using settings like these:
<configuration>
...
<system.xml.serialization>
<schemaImporterExtensions>
<add name="FileDataSchemaImporter" type="SchemaImporter.FileDataSchemaImporter, SchemaImporter, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=6c8e0bfd71c11c40" />
</schemaImporterExtensions>
</system.xml.serialization>
</configuration>
The type attribute is the important part. Make sure you use this format on a single line:
<Namespace-qualified class name>, <Assembly name without the extension>,
<Version>, <Culture>, <Public key token>
Now you're ready to use your schema importer. Try running wsdl.exe on the FileService:
http://localhost/WebServices2/FileService.asmx
The generated code proxy code will use the type name you specified and include the new namespace import. However, it won't create the FileData class-instead, you'll use the custom version you created in the FileData component.
Finally, add this generated proxy class to your client project. You can now download files with the chunk-by-chunk streaming support that's provided by the FileData class.
Note Currently, only the wsdl.exe uses schema importers. Schema importers don't come into play when you generate a web reference with Visual Studio. Expect this to change in later releases.
Summary
In this chapter, you took an in-depth look at the two most important web service protocols: SOAP and WSDL. SOAP is an incredibly lightweight protocol for messaging. WSDL is a flexible, extensible protocol for describing web services. Together, they ensure that web services can be created and consumed on virtually any programming platform for years to come. This chapter also discussed in detail how you can tailor the XML returned by your web service.
Book Details
 |
Pro ASP.NET 3.5 in C# 2008 |
By Matthew MacDonald , Mario Szpuszta |
ISBN10: 1-59059-893-8 |
Published Nov 2007 |
E-Book Price: $47.99 |
Price: $59.99 |