CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Peter's Gekko

public Blog MyNotepad : Imho { }

Web services on the world wild web

No more lamenting woes, back to real code. This was the core of my SDN presentation.

The web is an omnipresent infrastructure which your software can use to communicate. The way to communicate are XML webservices over HTTP. Any attempt to do it different often ends in a firewall as soon as it is deployed into the real world (wild web). In Visual Studio it's easy to set up a project which will serve a web service on your localhost and a consumer which interoperates with it. Such a service will communicate on the real web but some differences with your development setup will turn up.

  • Bandwidth. On a local machine, a local network or even a broadband flat-rate dsl it will be no a problem. But the internet also connects cell phones which have a far narrower path. Besides that on those branches of the web you often have to pay for every byte sent or received.
  • Latency. On your own machine or on a local network a response will be instantaneous. But when you're requesting something over the web it's not unusual to take a couple of seconds.

The main point is that the web is something you cannot control. A good web service is designed as something which interchanges messages over a not to reliable channel and not as just another way to call your object's methods. People like Christian Weyer often give very passionate presentations on this,  here's a photo impression on one. I'm not going to use his contract first tool here, nor WCF but just plain web services as offered by .NET right out of the box to show how you can do something about it there as well.

I assume you know the main things about setting up a web service project so I can jump right in. My demo web service publishes information from the Northwind database. It provides some bundled summary data of a customer based on an ID. The .NET way to bundle data is in a dataset. After designing the dataset a first shot at the web service could look like this.

[WebMethod]

public CustomerSummary GetCustomerSummary(string id)

{

    CustomerSummary ds = new CustomerSummary();

    CustomersTableAdapter ta = new CustomersTableAdapter();

    ta.FillBy(ds.Customers, id);

    return ds;

}

This will result in well typed customerinfo. A typical result when invoking it

 

<?xml version="1.0" encoding="utf-8" ?>

- <CustomerSummary xmlns="http://Gekko-Software.nl/SDN/SDE">

  - <xs:schema id="CustomerSummary" targetNamespace="http://MyCompany.com/CustomersSummary.xsd" xmlns:mstns="http://MyCompany.com/CustomersSummary.xsd" xmlns="http://MyCompany.com/CustomersSummary.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">

    - <xs:element name="CustomerSummary" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">

      - <xs:complexType>

        - <xs:choice minOccurs="0" maxOccurs="unbounded">

          - <xs:element name="Customers">

            - <xs:complexType>

              - <xs:sequence>

                - <xs:element name="CustomerID">

                  - <xs:simpleType>

                    - <xs:restriction base="xs:string">

                      <xs:maxLength value="5" />

                    </xs:restriction>

                  </xs:simpleType>

                </xs:element>

                - <xs:element name="CompanyName">

                  - <xs:simpleType>

                    - <xs:restriction base="xs:string">

                      <xs:maxLength value="40" />

                    </xs:restriction>

                  </xs:simpleType>

                </xs:element>

                - <xs:element name="ContactName" minOccurs="0">

                  - <xs:simpleType>

                    - <xs:restriction base="xs:string">

                      <xs:maxLength value="30" />

                    </xs:restriction>

                  </xs:simpleType>

                </xs:element>

                - <xs:element name="Phone" minOccurs="0">

                  - <xs:simpleType>

                    - <xs:restriction base="xs:string">

                      <xs:maxLength value="24" />

                    </xs:restriction>

                  </xs:simpleType>

                </xs:element>

              </xs:sequence>

            </xs:complexType>

          </xs:element>

        </xs:choice>

      </xs:complexType>

      - <xs:unique name="Constraint1" msdata:PrimaryKey="true">

        <xs:selector xpath=".//mstns:Customers" />

        <xs:field xpath="mstns:CustomerID" />

      </xs:unique>

    </xs:element>

  </xs:schema>

  - <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

    - <CustomerSummary xmlns="http://MyCompany.com/CustomersSummary.xsd">

      - <Customers diffgr:id="Customers1" msdata:rowOrder="0">

        <CustomerID>ALFKI</CustomerID>

        <CompanyName>Alfreds Futterkiste</CompanyName>

        <ContactName>Maria Anders</ContactName>

        <Phone>030-0074321</Phone>

      </Customers>

    </CustomerSummary>

  </diffgr:diffgram>

</CustomerSummary>

Which are quite a lot of bytes traveling over the wire. The main part of the result is the dataset's schema. The good thing is that the consumer does not need this schema on every invocation of the service. It can use the schema to build a type but that information is also available in the wsdl (Web Service Description Language). In .NET 2 you can exclude the schema form the result.

[WebMethod]

public CustomerSummary GetCustomerSummary(string id)

{

    CustomerSummary ds = new CustomerSummary();

    CustomersTableAdapter ta = new CustomersTableAdapter();

    ta.FillBy(ds.Customers, id);

    ds.SchemaSerializationMode = SchemaSerializationMode.ExcludeSchema;

    return ds;

}

Now the result is far smaller.

 

<?xml version="1.0" encoding="utf-8" ?>

- <CustomerSummary msdata:SchemaSerializationMode="ExcludeSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns="http://Gekko-Software.nl/SDN/SDE">

  - <xs:schema id="CustomerSummary" targetNamespace="http://MyCompany.com/CustomersSummary.xsd" xmlns:mstns="http://MyCompany.com/CustomersSummary.xsd" xmlns="http://MyCompany.com/CustomersSummary.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">

    - <xs:element name="CustomerSummary" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">

      - <xs:complexType>

        <xs:choice minOccurs="0" maxOccurs="unbounded" />

      </xs:complexType>

    </xs:element>

  </xs:schema>

  - <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">

    - <CustomerSummary xmlns="http://MyCompany.com/CustomersSummary.xsd">

      - <Customers diffgr:id="Customers1" msdata:rowOrder="0">

        <CustomerID>ALFKI</CustomerID>

        <CompanyName>Alfreds Futterkiste</CompanyName>

        <ContactName>Maria Anders</ContactName>

        <Phone>030-0074321</Phone>

      </Customers>

    </CustomerSummary>

  </diffgr:diffgram>

</CustomerSummary>

But it is still a very verbose way to return the small summary.

The ultimate mobile consumer is a smartphone. You can build smartphone applications with .net but these applications are based on the 1.1. version of the compact framework. CF 2.0 can consume a web service with typed datasets, but version 1 can not. It does understand untyped datasets. Changing the service contract to an untyped dataset would work.

[WebMethod]

public DataSet GetCustomerSummary(string id)

{

    CustomerSummary ds = new CustomerSummary();

    CustomersTableAdapter ta = new CustomersTableAdapter();

    ta.FillBy(ds.Customers, id);

    ds.SchemaSerializationMode = SchemaSerializationMode.ExcludeSchema;

    return ds;

}

The consumer does not have the many benefits of a strongly typed result but the amount of data going over the wire is still pretty large.

Thank goodness there is a more compact way to serialize a dataset. The GetXML() method returns a minimalist string representation of it's contents.

[WebMethod]

public string GetCustomerSummary(string id)

{

    CustomerSummary ds = new CustomerSummary();

    CustomersTableAdapter ta = new CustomersTableAdapter();

    ta.FillBy(ds.Customers, id);

    return ds.GetXml();

}

This will lead to a far smaller result message.

<?xml version="1.0" encoding="utf-8" ?>

<string xmlns="http://Gekko-Software.nl/SDN/SDE">

  <CustomerSummary xmlns="http://MyCompany.com/CustomersSummary.xsd">

    <Customers>

      <CustomerID>ALFKI</CustomerID>

      <CompanyName>Alfreds Futterkiste</CompanyName>

      <ContactName>Maria Anders</ContactName>

      <Phone>030-0074321</Phone>

    </Customers>

  </CustomerSummary>

</string>

The CF 1 consumer of the web service can deserialize this result into an untyped dataset.

private void GetCustomerSummary()

{

    CustomerService ws = new CustomerService();

    ws.Url = "http://192.168.1.90/SDE/SDE/CustomerService/CustomerService.asmx";

 

    string rxm = ws.GetCustomerSummary(textBox1.Text);

    StringReader sr = new StringReader(rxm);

    XmlTextReader xr = new XmlTextReader(sr);

    DataSet ds = new DataSet();

    ds.ReadXml(xr);

 

    label1.Text = ds.Tables["Customers"].Rows[0]["ContactName"].ToString();

    label2.Text = ds.Tables["Customers"].Rows[0]["CompanyName"].ToString();

}

After creating the proxy you have to set the URL to the service. A CF device nor the emulator understand localhost. Invoking the service returns a string; it takes a StringReader to read that and an XmlTextReader to deserialize that into a DataSet.

We have now found a way to reduce the precious bandwidth, but not yet done anything about the latency. The moment the smartphone starts invoking the webservice it's UI freezes until the web service returns. Not very nice to the user. The good thing is that the web service can be invoked asynchronously on a background thread. The bad thing is that there are some strings attached to that, especially in version 1 of the framework.

The proxy has a method to invoke the service on a background thread. This method is passed a callback function, when the result of the service returns this function will be called.

private void GetCustomerSummary()

{

    CustomerService ws = new CustomerService();

    ws.Url = "http://192.168.1.90/SDE/SDE/CustomerService/CustomerService.asmx";

    ws.BeginGetCustomerSummary(textBox1.Text, new AsyncCallback(wsCallBack), ws);

}

 

 

private void wscb(IAsyncResult asResult)

{

    // Implementation here

}

The BeginGetCustomerService method takes (in this case) three parameters. First comes the parameter to the web method. Second a delegate for the callback and third the proxy itself. The AyncCallback delegate needs a method with a signature like the wscb method. When a response from the web service is received this method will be called. The main mistake made in its implementation, also by me in the past, is that the method is running on the background thread, which is not the same one as the UI is running on. If you want to set UI properties, like a label caption, there's a chanchee you'll get threading exceptions. The safe way to reach the UI thread is by using the Invoke method. In the full framework there is the InvokeRequired property to query whether you need need Invoke or can reach the properties directly. In CF 1 this property is not available but you can bet you have to use invoke. Another hurdle is that the Invoke method in CF 1 cannot take any parameters, you can only invoke a method with the EventHandler signature (object sender, EventArgs e). The parameter values will be null. To pass some values a shared member is needed.

This leads to this implementation.

DataSet ds = new DataSet();

 

private void wscb(IAsyncResult asResult)

{

    customerhost.CustomerService ws = asResult.AsyncState as customerhost.CustomerService;

 

    string rawXml = ws.EndGetCustomerSummary(asResult);

    System.IO.StringReader sr = new StringReader(rawXml);

    XmlTextReader xr = new XmlTextReader(sr);

    lock (ds)

    {

        ds.Clear();

        ds.ReadXml(xr);

    }

 

    this.Invoke(new EventHandler(toonResultaat));

}

 

private void toonResultaat(object sender, EventArgs e)

{

    label1.Text = ds.Tables["Customers"].Rows[0]["ContactName"].ToString();

    label2.Text = ds.Tables["Customers"].Rows[0]["CompanyName"].ToString();

}

The dataset is now a private member. The callback function receives an IAsyncResult object and extracts the proxy object out of the AsyncState member. On this the EndGetCustomerSummary method is called to get the result of the service. The dataset has to be locked when writing to. A dataset is not threadsafe for writing and we are now on a different thread as the UI. To get to the UI thread the form's Invoke method is invoked. Which takes a delegate to the method which will update the UI.

So far I have presented a worst case scenario, a slow and narrow web consumed by a "primitive" CF 1 consumer. A 2.0 consumer has easier ways to do these things. But a non .net consumer can make life harder on you. The nice thing about the string representation of the XML is that you can use it as a base for XML DOM programming. The web service wrapper might get completely lost in (typed) datasets but with the tXmlDocument API (the name in Delphi) you can get anything in and out. For a deep-going example here's a story how Delphi 6 can assemble and dissemble diffgrams.

This is by far not the end. For instance I have assumed that a web service will always return something. The web is unreliable enough for that not to happen. Before jumping into that or any other "what if" scenario I suggest you take a look at WCF first as that has so much more to offer right out of the box. Web service in .NET <= 2 are just like the web itself. It usually works but don't count on it.

 


Published Sep 21 2006, 05:05 AM by pvanooijen
Filed under: ,

Comments

James Simmonds said:

And don't forget you can enable http compression in CF 1 with SP2 on your web service calls to compress the xml down. The best article on this is here...

http://blog.opennetcf.org/ayakhnin/PermaLink.aspx?guid=7ce7be20-ff09-4d38-b53f-018654c287d0

# September 21, 2006 6:20 AM

Peter's Gekko said:

In this post I&#39;m going to take a closer look at XSD, the XML schema language. Like most of you I

# August 28, 2007 3:33 PM

There is more to xsd than datasets « Tuff Stuff said:

Pingback from  There is more to xsd than datasets &laquo; Tuff Stuff

# August 29, 2007 7:20 PM

Leave a Comment

(required)  
(optional)
(required)  

Enter the numbers above:
Add
Check out Devlicio.us!

This Blog

Syndication

News