only for RuBoard |
A production version of the ZipService would probably use a database like SQL Server or Oracle to provide data for the service. To keep the example self-contained, an XML file (shown in Example 10-7) containing zip code information is used. The file contains only partial data for two cities, but can easily be extended if desired.
<?xml version="1.0" ?> <cities> <city name="Houston"> <zip>77002</zip> <zip>77003</zip> <zip>77004</zip> <zip>77005</zip> <zip>77006</zip> </city> <city name="Austin"> <zip>78742</zip> <zip>78744</zip> <zip>78746</zip> <zip>78748</zip> <zip>78750</zip> </city> </cities>
The implementation does not use the XML directly. Instead, a typed DataSet is created to handle data access. A DataSet provides a consistent relational programming model that is independent of a data source. This means that, once a DataSet is populated , where the data came from is irrelevant. By coding against a DataSet , the data source can be changed at a later time without negatively impacting the web service
The XML Schema Definition Tool that ships with the .NET Framework SDK ( xsd.exe ) does most of the work. This tool can generate an XSD schema that describes the XML in Example 10-7. In turn , that schema can build a derived DataSet class that allows the XML to be manipulated programmatically.
Building a DataSet with this tool is a three-phase process. The first step is to generate a schema from the XML. To do this, run xsd from the command line and pass the name of the XML file as an argument:
xsd zipcodes.xml
This produces a file called zipcodes.xsd that contains the schema shown in Example 10-8.
<?xml version="1.0" encoding="utf-8"?> <xs:schema id="cities" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="cities" msdata:IsDataSet="true"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="city"> <xs:complexType> <xs:sequence> <xs:element name="zip" nillable="true" minOccurs="0" maxOccurs="unbounded"> <xs:complexType> <xs:simpleContent msdata:ColumnName="zip_Text" msdata:Ordinal="0"> <xs:extension base="xs:string"> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="name" type="xs:string" /> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema>
If the DataSet were created directly from the schema, the following classes would be created: cities (the DataSet ), cityDataTable , cityRow , zipDataTable , and zipRow . Two event classes are also created, zipRowChangeEvent and zipRowChangeEvent . In essence, a class is created that represents a table in a database along with a corresponding class that represents a row of data from that table. Two events are defined that will be fired when data in those rows change. Internally, a parent-child relationship is defined between the two tables.
Because the zip is defined by an XML element, it gets the moniker zip_Text . The city name is described by the name attribute (as opposed to an element). So, to get a zip code, a call to zipRow.zip_Text is used, compared to getting a city name by calling cityRow.name . Very inconsistent!
Using the schema at this point to create a DataSet results in the generation of some very ugly source code, as the following testifies:
Dim city As cities.cityRow Dim row As System.Data.DataRow For Each row In cityZipDS.city.Rows city = CType(row, cities.cityRow) If city.name = "Houston" Then Dim zipRow As cities.zipRow Dim zipRows( ) As cities.zipRow = city.GetzipRows For Each zipRow In zipRows If zipRow.zip_Text = "77004" Then Return True End If Next zipRow End If Next row
The XSD really needs to be tweaked before the DataSet is created. Rather than thinking of the data in terms of data or a row, a collection analogy is much bettera Cities class that contains a collection of City that, in turn, contains a collection of Zipcode . Getting the city name and zip code through a consistent Text property would also be nice.
The second phase modifies the XSD output by the tool. Before anything can happen, an additional namespace must be added to the schema:
xmlns:codegen="urn:schemas-microsoft-com:xml-msprop"
This namespace makes several annotations available to the schema. Table 10-1 lists the ones that are pertinent to the discussion.
Annotation | Description |
---|---|
typedName | Object name |
typedPlural | Object collection name |
typedParent | Name of the object when used in a parent relationship |
typedChildren | Name of the method to return child objects |
Consider the city element here:
<xs:element name="city" codegen:typedName="City" codegen:typedPlural="Cities">
By using the typedName and typedPlural annotations with this element, the cityRow class becomes the City class and the Rows property of the primary DataSet class becomes Cities .
Now, when a city is used in a singular context, whether it is a property name or a class name, " City " is used. In the plural, " Cities " is used:
Dim cityZipDataSet As cities . . . Dim city As cities.City For Each city In cityZipDS.Cities 'Do something city-like here Next city
Example 10-9 contains the complete listing for the modified XSD that uses the typedName and typedPlural annotations.
<?xml version="1.0" encoding="utf-8"?> <xs:schema id="cities" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:codegen="urn:schemas-microsoft-com:xml-msprop"> <xs:element name="cities" msdata:IsDataSet="true"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="city " codegen:typedName="City " codegen:typedPlural="Cities"> <xs:complexType> <xs:sequence> <xs:element name="zip " nillable="true " minOccurs="0 " maxOccurs="unbounded " codegen:typedName="Zip " codegen:typedPlural="Zipcodes"> <xs:complexType> <xs:simpleContent msdata:ColumnName="zip_Text " msdata:Ordinal="0 " codegen:typedName="Text"> <xs:extension base="xs:string"> </xs:extension> </xs:simpleContent> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="name" type="xs:string " codegen:typedName="Text" /> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema>
Once the XSD has been modified to produce aesthetic output, the last phase is the creation of the DataSet . This is done by feeding the schema back to xsd.exe . This time, a few additional command-line parameters are necessary:
xsd /dataset /language:VB zipcodes.xsd
/dataset tells xsd to build a DataSet . An alternative switch is /classes , which causes a set of collection classes to be created. /language determines what language the output file will be written in. This will produce a file called zipcodes.vb that can be compiled with the web service.
|
Now that a typed DataSet exists, IsValid can finally be implemented. To get the XML into the cities dataset , an instance of StreamReader is used to create a file stream to the XML. The StreamReader is then used to create an XMLTextReader that reads the XML from the stream:
Dim cityZipDS As cities Dim xmlFile As String = AppDomain.CurrentDomain.BaseDirectory & _ "/bin/zipcodes.xml" Dim reader As New XmlTextReader(New StreamReader(xmlFile))
Finally, an XMLSerializer serializes the XML from the reader into cities , and the reader is closed:
Dim serializer As New XmlSerializer(GetType(cities)) cityZipDS = CType(serializer.Deserialize(reader), cities) reader.Close( )
At this point, navigating the data is similar to navigating a collection:
Dim city As cities.City For Each city In cityZipDS.Cities If city.Text = "Austin " Then Dim zipcode As cities.Zip Dim zipcodes( ) = city.GetZipcodes For Each zipcode In zipcodes If zipcode.Text = "78756" Then Return True End If Next zipcode End If Next ct
The final listing for ZipService is in Example 10-10. However, there is a major problem with the implementation. The data is reloaded into cities every time IsValid is called. Remember, calling a web method is just like SingleCall in remoting. The entire object is created and destroyed with every call. The challenge here is finding a way to cache the data so that performance will not be hindered. This implementation actually works well with a few simultaneous callers , but overall, it lacks scalability. There are probably several ways to fix this, but one suggestion is to move the implementation to a Singleton and make the web service a client of that object. The data is fairly static; new zip codes are not created every day. This way, the data would be loaded as needed. The ZipService class would really become a wrapper class to the Singleton implementation.
'vbc /t:library /r:System.dll '/r:System.Web.Services.dll ZipService.vb Imports System Imports System.Diagnostics Imports System.IO Imports System.Web.Services Imports System.Web.Services.Description Imports System.Web.Services.Protocols Imports System.Xml Imports System.Xml.Serialization <WebService(Namespace:="http://192.168.1.100/", _ Description:="This service provides city/zipcode authentication")> _ Public Class ZipService <WebMethod( ), SoapRpcMethod( )> _ Public Function IsValid(ByVal City As String, _ ByVal Zip As String) As Boolean Dim cityZipDS As cities Dim xmlFile As String = AppDomain.CurrentDomain.BaseDirectory + _ "/bin/zipcodes.xml" Dim reader As New XmlTextReader(New StreamReader(xmlFile)) Dim serializer As New XmlSerializer(GetType(cities)) cityZipDS = CType(serializer.Deserialize(reader), cities) reader.Close( ) Dim ct As cities.City For Each ct In cityZipDS.Cities If ct.Text = City Then Dim zipcode As cities.Zip Dim zipcodes( ) = ct.GetZipcodes For Each zipcode In zipcodes If zipcode.Text = Zip Then Return True End If Next zipcode End If Next ct Return False End Function End Class
only for RuBoard |