Chapter 17. Web Services and Remoting

Introduction

Web services are one of the most hyped features of .NET. They facilitate the sharing of information and services among companies and individuals on a programmatic level more elegantly than any other existing technology. The real benefit of using Web services in Visual Studio .NET is that you require nothing more than a simple URL to begin coding against a Web service residing on a remote server as if it were a local object. This cuts out the complexity of establishing network connections, formatting requests, and parsing replies.

From a business perspective, Web services can drastically reduce the development and integration time in rolling out affiliate programs. Online retailers can make their price listings available publicly via Web services to enable third parties to resell their products, knowing that items sold are currently in stock.

Although Web services may make information freely available, this does not imply that all of these services are free. Pay-per-use Web services such as SMS, credit card processing, and postal address lookup can be bought and used by third parties as part of larger applications with minimal effort.

Creating a Web service

In order to create a Web service, you will require access to an IIS server with the .NET framework installed. You will need administrative rights on this server to develop the Web service directly from Visual Studio .NET. When you install VS.NET, it will install and configure IIS for you.

This first Web service is used to report server variables from the server that hosts the service. This may not seem immediately useful, but one of the server variables (REMOTE_HOST) indicates the remote IP address of the client connecting to it. This information is useful to determine if a client is running behind a firewall or proxy, because in this case the local IP address on the client will not be the same as the IP address that would connect to a remote server. There is no easy way to determine this IP address using only code running on the client.

Another use of tracking the requester’s IP address is to limit the number of queries made against a service in any one day. This effectively prohibits data mining, but it could be a hindrance when many users use the service behind the same outgoing proxy or firewall.

Start a new project in Visual Studio .NET, selecting a project of type ASP.NET Web Service. The default path for this new project is on the local IIS server (http://localhost).

Note

If you receive an error concerning the “debugging users group” on IIS, this generally means you have not enabled Integrated Windows Authentication under Directory Security on the server.

A server will have many variables associated with it, although the names of these variables do not change from server to server; for flexibility, we can provide a method that returns an array of all the server variables stored on this machine.

Enter the following code in the asmx file:

C#

[WebMethod]
public String[] getServerVariableNames()
{
 System.Collections.Specialized.NameValueCollection col;
  col=Context.Request.ServerVariables;
  String[] arr = col.AllKeys;
  return arr;
}

VB.NET

<WebMethod> _
Public Function getServerVariableNames() As String()
  Dim col As _
    System.Collections.Specialized.NameValueCollection _
    col=Context.Request.ServerVariables
    Dim arr() As String = col.AllKeys
  Return arr
End Function

Notice the [WebMethod] attribute placed before the function name. This exposes the function for use over the Internet. The array returned from this method would be instantly recognizable by any ASP or ASP.NET developer. It would include strings such as REMOTE_HOST, representing the IP address of the client, and HTTP_USER_AGENT, representing the software being used by the client.

To retrieve the value of each of these variables, we can implement a second function as follows:

C#

[WebMethod]
public string[] getServerVariable(string variableName)
{
  System.Collections.Specialized.NameValueCollection col;
  col=Context.Request.ServerVariables;
  String[] arr = col.GetValues(variableName);
  return arr;
}

VB.NET

<WebMethod> _
Public Function getServerVariable(ByVal variableName As _
  String) As String()
  Dim col As _
  System.Collections.Specialized.NameValueCollection _
  col=Context.Request.ServerVariables
  Dim arr() As String = col.GetValues(variableName)
  Return arr
End Function

This function returns the value of a server variable when passed its name. It returns an array because some server variables return more than one result. To cite an example, HTTP_ACCEPT, the variable that enumerates the MIME types that the browser can render, will generally return an array of several different file types.

To test this service, run it from Visual Studio .NET, and you will see a browser open with an automatically generated Web page that details the public functions of your Web service. This Web page should be used for debugging purposes only because the default security setting is that the HTML interface is only available to browsers running on the local machine. End-consumers will use a programmatic interface to access this service. Click on getServerVariableNames and then Invoke.

You will see a new browser window opening with XML content, as shown in Figure 17.1. The XML is formatted as SOAP.

SOAP result returned from a Web service.

Figure 17.1. SOAP result returned from a Web service.

Take note of a server variable of interest, such as REMOTE_ADDR. Press Back on the first browser window, and select getServerVariable. Enter the name of the server variable in the box provided, and press Invoke. You will see a new window open and the Web service’s XML response encoded as SOAP.

You will notice the URL in the address bar changing as you navigate within this interface to the Web service. An HTTP GET request in the following format can be used to invoke a Web service method:

http://[ASMX file]/[function name]?[function parameters]

It is possible to use a GET request to invoke a Web service programmatically, but this is ill advised. Using the query string to pass objects is only possible with primitive types, and there are better ways to use a Web service programmatically.

Another HTTP GET request can be made against the Web service in the form

http://[ASMX file]?WSDL

This displays the formal definition of the Web service in Web Service Definition Language (WSDL) format. The WSDL definition allows Visual Studio .NET to determine the methods exposed by the Web service and to generate a suitable wrapper or proxy class for it. This step is generally done behind the scenes, but for the interested reader, it is possible to perform this step manually using the WSDL.EXE utility provided with .NET. The calling syntax is as follows:

WSDL http://[ASMX file]?WSDL

This will generate a C# proxy class in the same folder as WSDL.EXE. To generate a VB.NET class, precede the URL with /Language:VB.

Deploying a Web service

Having a Web service running on your local machine is fine for development purposes, but in order to make the service meaningful, it should be uploaded to a publicly accessible IIS server. Web services that are deployed publicly must have a unique namespace to distinguish them from other Web services on the Internet. Coding convention dictates that the namespace should be in the form of a domain name that you control. The namespace may look like a URL, but it does not need to point to anything on the Web in particular.

C#

[WebService(Namespace="http://www.myserver.com/")]

VB.NET

<WebService(Namespace:="http://www.myserver.com/")> _

If you want to make it easy for people to find your Web service, one of the first places you should advertise it is at http://uddi.Microsoft.com or http://test.uddi.Microsoft.com. These are public repositories for Web services and generally the first place developers go when looking for a particular online service.

Universal description discovery integration (UDDI) is an open standard that can be accessed programmatically by using the Microsoft.Uddi.Sdk namespace provided with the UDDI SDK.

Using a Web service

As mentioned earlier, the automatically generated Web interface for a Web service is not designed for public use. Instead, you generate a proxy class that accesses the service programmatically, and you can code against the Web service as if you are using a local object.

In Visual Studio .NET, you don’t need to code a proxy class yourself; it will be created for you. All you need to do is enter the URL of the Web service, and all of the behind-the-scenes work is taken care of.

Start a new project in Visual Studio .NET and select Windows Forms Application. Click Project→Add Web Reference, and then enter the URL of the ASMX file created in the previous example. Press Add Reference once you have found the Web service. In the following example, the Web service is assumed to reside on the local machine and to be named Service1.

Draw a list view on the form, and name it lvServerVariables. A button named btnPopulate is also required.

Click on the form and add the following code:

C#

private void Form1_Load(object sender, System.EventArgs e)
{
  lvServerVariables.View=View.Details;
  lvServerVariables.Columns.Add("Name",
     lvServerVariables.Width/2,
     HorizontalAlignment.Left);
  lvServerVariables.Columns.Add("Value",
     lvServerVariables.Width/2,
     HorizontalAlignment.Left);
}

VB.NET

Private Sub Form1_Load(ByVal sender As Object, ByVal _
  e As System.EventArgs)
  lvServerVariables.View=View.Details
  lvServerVariables.Columns.Add("Name", _
     lvServerVariables.Width/2, _
     HorizontalAlignment.Left)
  lvServerVariables.Columns.Add("Value", _
     lvServerVariables.Width/2, _
     HorizontalAlignment.Left)
End Sub

This code simply lays the list view out on the screen in a neat way, with the column headers equally spaced across the screen.

Click on the Populate button, and add the following code:

C#

private void btnPopulate_Click(object sender,
System.EventArgs e)
{
  string[] serverVariableNames;
  localhost.Service1 webservice = new localhost.Service1();
  serverVariableNames = webservice.getServerVariableNames();
  lvServerVariables.Items.Clear();
 foreach (string serverVariableName in serverVariableNames)
 {
    ListViewItem lvItem = new ListViewItem();
    lvItem.Text = serverVariableName;
    string[] serverVariableValues;
    serverVariableValues =
    webservice.getServerVariable(serverVariableName);
    if (serverVariableValues!=null)
    {
      lvItem.SubItems.Add(serverVariableValues[0]);
    }
 lvServerVariables.Items.Add((ListViewItem)lvItem.Clone());
}
}

VB.NET

Private Sub btnPopulate_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs)
  Dim serverVariableNames() As String
  Dim webservice As localhost.Service1 = New _
    localhost.Service1
  serverVariableNames = webservice.getServerVariableNames()
  lvServerVariables.Items.Clear()
  Dim i As Integer
  For each serverVariableName as string in _
    serverVariableNames
    Dim lvItem As ListViewItem = New ListViewItem
    lvItem.Text = serverVariableName
    Dim serverVariableValues() As String
    serverVariableValues = _
       webservice.getServerVariable(serverVariableName)
    If Not serverVariableValues Is Nothing Then
      lvItem.SubItems.Add(serverVariableValues(0))
    End If
    lvServerVariables.Items.Add(CType(lvItem.Clone(), _
      ListViewItem))
  Next
End Sub

This code would seem to have nothing to do with networking code, but in fact, it communicates extensively with the remote server via the proxy class every time a method is called on the webservice object.

If you would like to view the proxy class, you can click on show all files in the Solution Explorer, and click Localhost→Reference.map→Reference.cs. It is not advisable to edit the proxy class manually.

The rest of the code above is concerned with displaying the data returned from the Web service on-screen. Only the first element in the array returned from getServerVariable is actually rendered on-screen, for the sake of simplicity.

To test the Web service client, run it from Visual Studio .NET, ensure that IIS is running on the local machine, and then press Populate. You should see a list appearing on-screen, which should resemble Figure 17.2.

Web service client application.

Figure 17.2. Web service client application.

Asynchronous calls to Web services

If the same Web service were deployed on several geographically separated Web servers, clients could connect to several Web services at once in order to improve performance. This may only be applicable in situations where several calls have to be made and each call takes a significant amount of time to complete.

To understand the scenario, we could envisage a situation where an application displays live stock values of a large share portfolio. A Web service is hosted on a server in the United States, which is linked into the NASDAQ exchange, and another server is located in Japan, which is linked into the Nikeii exchange. A customer in question has shares in Microsoft and Toyota. If the client were to issue a request for the value of the Microsoft shares, wait for the response, and then request the value of the Toyota shares, the process would take twice as long as if both requests were made simultaneously.

Several techniques can be used to manage simultaneous Web service calls. The following code examples perform the same function: They make two calls to a Web service and measure the response times to the calls. IIS is multithreaded, so it handles both of these requests in parallel. In a real-world example, the same Web service would be mirrored on more than one server, so that the two requests would be handled at exactly the same time.

Each of the following samples requires a simple user interface consisting of only a button and a label. To create this interface, open a new project in Visual Studio .NET, and select a Windows form application. Draw a button on the form and name it btnMakeCall and then draw a label named lblStatus.

You will also require a Web reference to the Web service as described earlier in this chapter. This Web reference should be named localhost, for the purposes of these code examples. The Web service does not necessarily need to be hosted on the local machine.

Wait handles

A wait handle is equivalent to a do-nothing while loop using polling, but it is less processor intensive. This should only be used in a separate thread, or the client application will be nonresponsive to the user. This technique should only be used when useful client-side processing can be performed before data is returned from any of the Web services.

Click on the Make Call button and enter the following code:

C#

private void btnMakeCall_Click(object sender,
System.EventArgs e)
{
  long timeStart = DateTime.UtcNow.Ticks;
  localhost.Service1 svc = new localhost.Service1();
  IAsyncResult result1;
  IAsyncResult result2;
  result1 = svc.BegingetServerVariableNames(null,null);
  result2 =
  svc.BegingetServerVariable("REMOTE_ADDR",null,null);
  result1.AsyncWaitHandle.WaitOne();
  result2.AsyncWaitHandle.WaitOne();
  string[] varNames = svc.EndgetServerVariableNames(result1);
  string[] response = svc.EndgetServerVariable(result2);
  lblStatus.Text = "Time elapsed:" +
  (DateTime.UtcNow.Ticks - timeStart);
  lblStatus.Text += " ticks";
}

VB.NET

Private Sub btnMakeCall_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs)
  Dim timeStart As Long = DateTime.UtcNow.Ticks
  Dim svc As localhost.Service1 = New localhost.Service1()
  Dim result1 As IAsyncResult
  Dim result2 As IAsyncResult
  result1 = svc.BegingetServerVariableNames( _
      Nothing,Nothing)
  result2 = _
     svc.BegingetServerVariable( _
     "REMOTE_ADDR",Nothing,Nothing)
  result1.AsyncWaitHandle.WaitOne()
  result2.AsyncWaitHandle.WaitOne()
  Dim varNames() As String = _
     svc.EndgetServerVariableNames(result1)
  Dim response() As String = _
     svc.EndgetServerVariable(result2)
  lblStatus.Text = "Time elapsed:" & _
     (DateTime.UtcNow.Ticks - timeStart)
  lblStatus.Text += " ticks"
End Sub

To test this code, run the application from Visual Studio .NET, and press the make Call Button. The user interface will become unresponsive until the call is received. In a production environment, the code detailed above should be contained within a separate thread.

Callbacks

Callbacks produce the least amount of processor overhead while waiting for Web service calls to return. They are ideal in situations where no useful client-side processing can be performed before all of the data is received; however, it could be difficult to determine when the last call has returned successfully or erroneously.

Click on the Make Call button and enter the following code:

C#

  public localhost.Service1 svc;
  public long timeStart;
  private void btnMakeCall_Click(object sender,
  System.EventArgs e)
  {
  timeStart = DateTime.UtcNow.Ticks;
  svc = new localhost.Service1();
  svc.BegingetServerVariableNames(new
  AsyncCallback(ServiceCallback1),null);
  svc.BegingetServerVariable("REMOTE_ADDR",new
  AsyncCallback(ServiceCallback2),null);
}

private void ServiceCallback1(IAsyncResult result)
{
  string[] response = svc.EndgetServerVariableNames(result);

  lblStatus.Text = "Time elapsed:" +
  (DateTime.UtcNow.Ticks - timeStart);
  lblStatus.Text += " ticks";
}

private void ServiceCallback2(IAsyncResult result)
{
  string[] response = svc.EndgetServerVariable(result);
  lblStatus.Text = "Time elapsed:" +
  (DateTime.UtcNow.Ticks - timeStart);
  lblStatus.Text += " ticks";
}

VB.NET

Public svc As localhost.Service1
Public timeStart As Long
Private Sub btnMakeCall_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs)
  timeStart = DateTime.UtcNow.Ticks
  svc = New localhost.Service1()
  svc.BegingetServerVariableNames(New _
     AsyncCallback(AddressOf ServiceCallback1),Nothing)
  svc.BegingetServerVariable("REMOTE_ADDR",New _
     AsyncCallback(AddressOf ServiceCallback2),Nothing)
End Sub
Private Sub ServiceCallback1(ByVal result As IAsyncResult)
  Dim response() As String = _
     svc.EndgetServerVariableNames(result)
  lblStatus.Text = "Time elapsed:" & _
  (DateTime.UtcNow.Ticks - timeStart)
  lblStatus.Text += " ticks"
End Sub

Private Sub ServiceCallback2(ByVal result As IAsyncResult)
  Dim response() As String = _
    svc.EndgetServerVariable(result)
  lblStatus.Text = "Time elapsed:" & _
    (DateTime.UtcNow.Ticks - timeStart)
  lblStatus.Text += " ticks"
End Sub

To test this code, run the application from Visual Studio .NET, and press the Make Call button. The time displayed is the time that has elapsed between the issuing of the Web methods and the last response received. A more robust solution would be to use a global array that would track the progress of each call.

The BeginGetServerVariableNames function takes two parameters; the first parameter indicates the procedure to be called once the web-method returns, and the second, as shown in the code example above, is set to null or Nothing. This parameter can optionally contain objects that can be passed to the callback via the result object.

Interoperability

When developing a Web service, it should be straightforward for any developer working on any platform to implement a client. The previous example should demonstrate that it is easy to implement a Web service client in .NET, but if your service is to be made available to third-party Web site developers, you have to make sure that you do not needlessly complicate their job simply for the sake of using this new buzzword in Web technology.

Although it may not seem like your responsibility to support third-party developers that integrate into your software, it would be lunacy (and bad for business!) to provide a service that was so difficult to use from platforms other than .NET that developers would simply give up and find a different supplier.

Most languages now support XML. With this, it is easy to extract primitive types such as strings, numbers, and arrays from SOAP responses; however, if complex objects such as datasets and nested classes are rendered as SOAP, it is likely that the average PHP Web developer will throw his hands up in despair. Therefore, if it is envisaged that there may be a user base that may not use Microsoft scripting languages to run their Web sites, then the clarity of XML returned from Web service methods should be closely examined.

If the third party wishing to access your Web service is running a Microsoft platform and does not intend to use .NET (e.g., if he or she are using classic ASP or Visual Basic 6), then you cannot force these people to migrate to .NET in order to use your Web service; however, you could mention the SOAP toolkit from Microsoft (msdn.microsoft.com/webservices/building/soaptk/), which can greatly simplify the task of adding Web service support to a legacy Windows application.

Performance

The first thing that may strike you when running the code sample above is that it can take several seconds to populate a short list of information. Web services are slow on first access because of background .NET compilations. It may look as if Web services were designed more for interoperability than speed.

In chapter 4, remoting was discussed. This technology is similar to Web services. With remoting, there were many ways to improve performance by using more simplistic protocols. With Web services, there is no easy way to use anything other than SOAP. Having said this, the one-protocol-only way of doing things makes life easier for system integrators who are working on different platforms. The trade-off between interoperability and performance has to be decided on a case-by-case basis. It should be clear enough that SOAP is more interoperable than Microsoft’s proprietary binary format.

In benchmarking tests, a Web service and remoting object both made queries to a database in response to client requests. Under high-load conditions (60 requests per second for a single database entry), a remoting object hosted on a Windows service using a binary formatter over TCP outperformed the Web service by 50%.

Although remoting objects can be configured for higher performance than Web services, when a remoting object communicates with SOAP over HTTP, it is actually slower than a Windows service by about 25% under the same load as stated above. Furthermore, it is more difficult to use a remoting object than a Web service because there is no automatic mechanism to discover the interface of a remoting object, whereas Web services use WSDL. Some other configurations of the remoting object also succeeded in outperforming the Web service. They were binary format over HTTP and SOAP format over TCP.

When a remoting object is hosted on IIS rather than in a Windows service, the performance level drops substantially. When a remoting object uses the binary format, it only barely surpasses Web services performance at 20 requests per second; however, using other configurations, such as SOAP over HTTP on IIS, dropped the performance to 35% under Web services.

To sum up, in order to achieve maximum performance, with a user base that is exclusively .NET clients, and where you have access to a dedicated Windows server, then use a Windows service hosting a remoting object using binary format over TCP. If the user base could include non-.NET clients, however, or if you have only shared access to a server, then you should use a Web service.

Security

Web services run on IIS servers, so an IIS server with SSL certificates installed provides secure Web services. This rather simplistic view of security in Web services is nonetheless probably the best approach to take when implementing a secure Web service at the moment.

Web site security is more concerned with ensuring that the server is authenticated to the client than vice versa, but this makes good sense because it means that customers will know they are giving their credit card details to a reputable supplier, but the supplier doesn’t really care who enters the details, as long as money is involved in the transaction.

With Web services, the typical user would have paid for the privilege of using the service. The user would not care exactly who is providing the service, just that the information is correct; however, the Web service provider would need to know that the client was in fact a paying customer.

HTTPS provides for client authentication, so there is no need to reinvent the wheel here. In an intranet environment, a Windows authentication system will undoubtedly already be in place on the network. To provide credentials with a Web service call, it is a matter of setting the Credentials property of the Web service, such as in the following code snippet:

C#

 localhost.Service1 webservice = new localhost.Service1();
 CredentialCache cache = new CredentialCache();
 NetworkCredential netCred =
       new NetworkCredential( "user", "pass", "myServerName"
);
cache.Add( new Uri(svc.Url), "Basic", netCred );
webservice.Credentials = cache;

VB.NET

Dim webservice As localhost.Service1 = New _
   localhost.Service1()
Dim cache As CredentialCache = New CredentialCache()
NetworkCredential netCred = _
   New NetworkCredential("user", "pass", "myServerName")
cache.Add(New Uri(webservice.Url), "Basic", netCred)
webservice.Credentials = cache

On the Web service side, it is possible to check credentials using the following statement:

Thread.CurrentPrincipal.Identity.Name

Which will return either an empty string or a user name in the following form:

[Domain][user]

Of course, this type of authentication is only useful for intranet situations. It is not applicable for globally accessible services when SSL is not used on the server. The best practice is to use client X.509 certificates, but this would be overkill for everything less than financial applications because it takes a lot of time and effort to get issued an X.509 client certificate with your name on it. An X.509 certificate can be included in the client request by adding it to the ClientCertificates collection thus:

C#

localhost.Service1 webservice = new localhost.Service1();
X509Certificate x509 = X509Certificate.CreateFromCertFile(
    "c:\myCertificate.cer");
webservice.ClientCertificates.Add(x509);

VB.NET

Dim webservice As localhost.Service1 = New _
   localhost.Service1()
X509Certificate x509 = X509Certificate.CreateFromCertFile( _
   "c:myCertificate.cer")
webservice.ClientCertificates.Add(x509)

If your Web service needs to be secure enough to prevent nonpaying users from accessing it, but doesn’t require the overhead of end-to-end strong encryption, an acceptable middle road is to use hashing, or as it is more correctly called, digest authentication. This is where each customer is allocated a username and password. The password is combined with the username and then hashed. The hash digest is then sent as a parameter to the Web method. If the digest matches the hash of the username and password pair held in the database, then the user can be authenticated. To increase security, a second digest could be created, composed from the current time (accurate to the minute) and the user’s password. A hashed timestamp more than one minute old would be rejected. This means that a hacker listening on the wire could not record and replay Web service requests.

Web services enhancements

Web services can be made more flexible by installing Web Services Enhancements (WSE) from Microsoft. To save confusion over terminology, Global XML Web Services Architecture (GXA) was a joint proposal by IBM and Microsoft. WSE is Microsoft’s adaptation of GXA, which is, for all intents and purposes, identical. The added features are attachments, security, routing, and referral.

WSE can be downloaded from http://msdn.microsoft.com/webservices/building/wse. Once installed, it can be integrated into any .NET project by adding a reference to Microsoft.Web.Services.dll and by modifying the Web.Config file for the project by adding a type to soapExtensionTypes thus:

<configuration>
 <system.web>
  ...
  <webServices>
   <soapExtensionTypes>
    <add type= "Microsoft.Web.Services.WebServicesExtension,
     Microsoft.Web.Services,
     Version=1.0.0.0,
     Culture=neutral,
     PublicKeyToken=31bf3856ad364e35"
     priority="1" group="0" />
   </soapExtensionTypes>
  </webServices>
 </system.web>
</configuration>

Web service extensions: Attachments

If your Web service returns multimedia data, such as images or audio, you should consider using SOAP attachments. Including binary data as a SOAP attachment as distinct from plain text offers a performance advantage because the data will not be encoded and bloated in size. SOAP attachments use the direct Internet message encapsulation (DIME) format. This feature is included in WSE 1.0. Only the core features of the technology are described here.

To attach an image (such as c:photo.jpg) to a SOAP response, you could use code similar to the following:

C#

string filePath = "C:\myPhoto.jpg";
DimeAttachment dimeImage = new DimeAttachment(
   "image/jpeg", TypeFormatEnum.MediaType,
   filePath);
dimeImage.Id = "uri:" + Guid.NewGuid().ToString();
SoapContext cntxt = HttpSoapContext.ResponseContext;
cntxt.Attachments.Add(dimeImage);

VB.NET

Dim filePath As String = "C:myPhoto.jpg"
DimeAttachment dimeImage = New DimeAttachment( _
    "image/jpeg", TypeFormatEnum.MediaType, _
     filePath)
dimeImage.Id = "uri:" & Guid.NewGuid().ToString()
Dim cntxt As SoapContext = HttpSoapContext.ResponseContext
cntxt.Attachments.Add(dimeImage)

You will require the following namespaces:

C#

using System.Web.Services;
using Microsoft.Web.Services;
using Microsoft.Web.Services.Dime;

VB.NET

Imports System.Web.Services
Imports Microsoft.Web.Services
Imports Microsoft.Web.Services.Dime

The Web service client could extract the image data from the SOAP response by using the following code:

C#

localhost.Service1 webservice = new localhost.Service1();
Stream attachment =
webservice.ResponseSoapContext.Attachments[0].Stream;
Bitmap myImage = new Bitmap(attachment);

VB.NET

Dim webservice As localhost.Service1 = New _
   localhost.Service1()
Dim attachment As Stream
Attachment = _
   webservice.ResponseSoapContext.Attachments(0).Stream
Dim myImage As Bitmap = New Bitmap(attachment)

There are several limitations to DIME in WSE 1.0. One significant limitation is that SOAP attachments are not reflected in the WDSL contract that is generated with the Web service. This means that clients will not be aware, until they make a request to your Web service, that there are any attachments in the response. Furthermore, DIME is not portable among different platforms and is proprietary to Microsoft. To make matters worse, COM clients using the SOAP toolkit will not be able to access attachments at all unless the WDSL is manually edited to contain the appropriate <dime:message> child elements and <wsdl:output> elements, as described in the WDSL specification.

Another limitation of DIME in WSE 1.0 is that security does not extend to the attachment. Therefore, whenever attachments need to be kept secure from prying eyes and made resistant to man-in-the-middle tampering, you will have to implement your own hashing and encryption mechanism. Alternately, as previously recommended, the Web service should run over SSL to provide end-to-end encryption and avoid security loopholes such as this one.

Web service extensions: Routing

When a Web service begins to scale upward, it may quickly outgrow a single-server environment and require hosting on several servers in parallel. Because Web services run over IIS, they can be scaled upward in much the same way as any Web site. This includes using load-balancing systems such as Cisco Local Director or Microsoft NLB.

Load-balancing systems do generally delegate workload equally among servers, and sometimes you may require more logic behind the load balancing. When talking specifically about Web services, you can use WSE to create an intermediary Web service. This Web service could be used to direct Web service calls to other servers, which may contain more up-to-date data or be otherwise more appropriate for that particular call.

A word on Project Hailstorm (MyServices)

Project Hailstorm, or MyServices, is a technology that was shelved by Microsoft in early 2002; therefore, it is best avoided. MyServices was a project put forward by Microsoft to permit people to store data they would use on a day-to-day basis on their servers via an array of custom-built Web services. Services such as .NET Contacts to store your personal address book, .NET Inbox to store your email, and .NET Wallet to store your credit card details would be available through MyServices. The idea is technically sound, but many people and companies balked at the idea of Microsoft being in control of so much personal information.

.NET remoting

Remoting is .NET’s equivalent of Java remote method invocation (RMI) and Visual Basic’s Distributed Common Object Model (DCOM). It facilitates the use of complex objects on remote computers, using the same syntax as if they were in the same application. The advantage that remoting affords is the abstraction of the network infrastructure. This greatly simplifies the implementation of client/server applications in which the server must perform a variety of tasks based on instructions from the client.

Imagine a scenario in which a distributed billing system is being developed, where the client’s systems are high-street hire-purchase outlets, and a central server at the head office handles customer invoicing, debt collection, and so forth. Clients would require the server to perform tasks such as perform credit check, record start of lease, terminate lease, process credit card payment, and other such tasks. Of course, the same effect could be achieved by sending strings over TCP/IP, which the server would parse it on the remote side, but it is simpler to make a call to customer.terminateLease() and let .NET handle the network transmission.

How remoting works

When using remoting, you still need to create a client and server. You also need to create an object that performs whatever functions you require. Both ends of the connection need to know the type of the object. The client needs to know the IP address and port of the server. Other than that, .NET does the rest.

Although you don’t see what is being passed over the network, you do have a choice whether to go for SOAP over HTTP (portable) or binary over TCP (performance). SOAP used for remoting differs from the industry format somewhat and would be less portable than an equivalent Web service.

Note

Channel sinks can be used to view or modify the data immediately before it is sent across the wire. This can be used to add security or queuing features.

To prevent clients from draining the server’s resources by creating millions of objects and abandoning their instances, remoting has a built-in garbage-collection system. Objects can be created so that their lifetime lasts only as long as the execution time of the function (singlecall) or as long as the class (singleton) or a server-defined lifetime (published objects). Remote object lifetimes, with the exception of published objects, are specified in the call to RemotingConfiguration.RegisterWellKnownServiceType, as we shall see later.

Published objects are instantiated slightly differently, where, instead of the call to RegisterWellKnownServiceType, the object is created thus:

C#

RemoteObject obj = new RemoteObject(1234);
RemotingServices.Marshal (obj,"RemotingServer");

VB.NET

Dim obj as RemoteObject
obj = new RemoteObject(1234);
RemotingServices.Marshal(obj,"RemotingServer")

After which the object behaves as a singleton. The benefit of creating an object in this way is that it is possible to create objects with nondefault constructors. This could include constructors that require user intervention and, thus, are unsuitable for arbitrary client activation.

The key to remoting is to create a class that derives from MarshalBy-RefObject. This object is then capable of running within the context of a server and exposes its methods and properties through that server. While running in the context of the server, local resources such as files and databases located on the server are accessible through the class. Objects that are returned as a result of calling methods on this class, however, are run in the context of the client. These objects are called By Value objects.

By Value objects cannot access server resources, such as databases or files; however, they can be prepopulated with data taken from server resources such as these. For instance, the ubiquitous DataSet is perfectly acceptable as a By Value object. A remote object returns a By Value object by serializing it and transferring it over the network to the client. This mechanism will only work if two conditions are met: (1) the object must be marked [Serializable] or implement ISerializable, and (2) the client must hold at the metadata for the By Value object, such that it can correctly deserialize it.

Implementing remoting

This example demonstrates a simple remoting application, where the client application may request a number from a server, which is incremented on every call.

Start a new Class library project in Visual Studio .NET, and enter the following code:

C#

using System;
namespace RemoteObject
{
  public class IDGenerator : System.MarshalByRefObject
  {
  private int lastID =0;
  public int getID()
  {
    return(lastID++);
  }
 }
}

VB.NET

Imports System
Namespace RemoteObject
  Public Class IDGenerator
   Inherits System.MarshalByRefObject
    Private lastID As Integer = 0
    Public Function getID() As Integer
      lastID = lastID + 1
                   return(lastID)
    End Function
  End Class
End Namespace

You will note that the class derives from System.MarshalByRefObject. This enables the object to be transferred over a remoting channel.

Compile the object, and note the location of the resultant DLL. The next step is to create the server application to host this object.

Create a new Windows form project in Visual Studio .NET. Click Project→Add References→Browse, and then click on the DLL created in the last compilation. You will also need to select the System.Runtime.Remoting namespace.

C#

private void Form1_Load(object sender, System.EventArgs e)
{
  HttpChannel channel = new HttpChannel(8085);
  ChannelServices.RegisterChannel(channel);
  RemotingConfiguration.RegisterWellKnownServiceType(
        typeof(RemoteObject.IDGenerator),
        "RemotingServer",
        WellKnownObjectMode.Singleton);
}

VB.NET

Private Sub Form1_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
  Dim channel As HttpChannel = New HttpChannel(8085)
  ChannelServices.RegisterChannel(channel)
  RemotingConfiguration.RegisterWellKnownServiceType( _
  (New RemoteObject.RemoteObject.IDGenerator).GetType(), _
           "RemotingServer", _
           WellKnownObjectMode.Singleton)

End Sub

Certain things can be immediately ascertained by looking at this code. The communications will take place on port 8085, using SOAP over HTTP. The object is to be created as a Singleton, which means that it is state-full, and the value of LastID will be maintained between calls.

You will also require the supporting namespaces:

C#

using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using RemoteObject;

VB.NET

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports RemoteObject

Create a new Windows forms project in Visual Studio .NET. Click Project→Add References, click Browse, and then click on the DLL created in the last compilation. Draw a button on the form and name it btnGetID. Now click on the btnGetID button and enter the following code:

C#

private void btnGetID_Click(object sender, System.EventArgs
e)
{
 RemoteObject.IDGenerator remObject =
(RemoteObject.IDGenerator)Activator.GetObject(
        typeof(RemoteObject.IDGenerator),
        "http://localhost:8085/RemotingServer");
 if (remObject==null)
  MessageBox.Show("cannot locate server");
 else
  MessageBox.Show(Convert.ToString(remObject.getID()));
}

VB.NET

Private Sub btnGetID_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs)
  Dim remObject As RemoteObject.IDGenerator = _
     CType(Activator.GetObject( _
     (New RemoteObject.IDGenerator).GetType(), _
     "http://localhost:8085/RemotingServer"), _
           RemoteObject.IDGenerator)
  If remObject Is Nothing Then
    MessageBox.Show("cannot locate server")
  Else
    MessageBox.Show(Convert.ToString(remObject.getID()))
  End If
End Sub

In this code, the call to the remote object is discretely written as remObject.getID(). It is worthwhile to note that this is a synchronous call, and if the client could be doing other things while waiting for the method to return, then either an asynchronous or one-way call should be employed, as explained later.

Again, you will also require the supporting namespaces:

C#

using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using RemoteObject;

VB.NET

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports RemoteObject

To test the application, execute the client and server together (Figure 17.3), and then press the button on the client a few times. You will see the number in the message box increasing.

Remoting Client and remoting Server.

Figure 17.3. Remoting Client and remoting Server.

Asynchronous use of remote objects

Asynchronous use of remote objects can be achieved by using delegates, which are the .NET equivalent of function pointers. They are declared within the same class as the client, but outside any of its methods. It would have the same function prototype as the synchronous method you wish to call. For instance, a remote method named getDetails() returning string would have a corresponding delegate such as the following:

C#

delegate String GetDetailsDelegate();

VB.NET

Delegate Function GetDetailsDelegate() as string

Assuming the remote object is already instantiated and named obj, the getDetails() method can be called thus:

C#

GetDetailsDelegate gdDelegate = new
GetDetailsDelegate(obj.GetDetails);
IASyncResult gdAsyncres = gnDelegate.BeginInvoke(null,null);

VB.NET

Dim gdDelegate as GetDetailsDelegate
Dim gdAsyncres as IASyncResult
gdDelegate = new GetDetailsDelegate(AddressOf obj.GetDetails)
gdAsyncres = gnDelegate.BeginInvoke(nothing,nothing)

This code returns immediately, and the server will begin executing the GetDetails method on the remote object. In order to retrieve the return value from the call, the client must execute EndInvoke on the delegate. This method is blocking and will only return once the server has responded. It is called as follows:

C#

String details = gdDelegate.EndInvoke(gnAsyncres);

VB.NET

Dim details as string
Details = gdDelegate.EndInvoke(gnAsyncres)

Although this method should be sufficient for most purposes, there is another way to invoke a remote object asynchronously, by using the OneWay attribute. One-way calls are made in the same way as standard asynchronous calls from the client; however, the EndInvoke method will be nonblocking and is guaranteed to return immediately, whether the server has responded or not. This is useful for noncritical “call-and-forget” methods, where overall application speed is more important than guaranteeing execution of selected peripheral functions.

To implement a one-way function, simply mark a method within the interface definition with the attribute [OneWay()].

Deployment of a remoting service

When using remoting in a commercial application, a few tricks of the trade help your software become more robust and manageable. The client must be able to tell the type of the object it is to receive at compile time. This means that if you have already deployed your client to a million users worldwide, you can’t make changes to the object or all of the clients will stop working. The way around this dilemma is to have the client refer to the interface of the object rather than the object itself, which means you can change the implementation of the object’s methods without breaking compatibility. Perhaps a more important aspect is that if you are sharing the implementation of your software with third parties, they could possibly decompile or otherwise reverse-engineer your DLL, using ILDASM or MSIL-to-C# (www.saurik.com).

An interface to the RemoteObject.IDGenerator class above is as follows:

C#

using System;
public Interface IIDGenerator
{
  public int getID();
}

VB.NET

Imports System
Public Interface IIDGenerator
  Public Function NextOrder() As int
End Interface

Using shared interfaces is not the only way to provide clients with access to remote objects. The two main drawbacks are that third parties working with your remote object must be sent the new interface whenever any of the public methods or properties in the object change; furthermore, you cannot pass these objects as parameters to functions running in a different context.

An alternate method is to use shared base classes. This is where the client is provided with an abstract base class. The server would inherit from this base class and implement the required functionality. This would make these classes capable of being passed to functions running in different contexts; however, it is impossible to create new objects using the new operator, only the Activator.GetObject() can be used in this instance.

In order to address the deployment issue, Microsoft has created a utility named soapsuds. This command-line utility can be used to extract metadata from remote objects. It is invoked from DOS thus:

soapsuds -url:<URL>?wsdl -oa:<OUTPUT>.DLL -nowp

This generates a DLL file, which client programs can use to communicate with the remote object. This DLL does not contain any implementation details, only interfaces. The -nowp, or no-wrap, parameter is used to indicate whether the URL of the remote object should be hard-coded into the DLL. An unwrapped proxy DLL does not contain URL information, but a wrapped proxy DLL does. The benefit of hard-coding the URL into the DLL is that the remote object can be created using the new operator. Otherwise, the client has to use Activator.GetObject() to instantiate the object.

Configuration

One major issue regarding deployment of remoting services is the ability to configure clients quickly and easily. For instance, if you are forced to change the IP address of the server hosting the remote object, it could be tricky to change the code to point to the new IP address, recompile the application, and request that all customers upgrade their software. A more acceptable solution is to hold the location of the remote object in a separate XML file, which could be replaced with a hot-fix patch when required. Therefore, .NET provides a means of providing configuration files for remoting clients. A configuration file takes the following form:

XML

<configuration>
 <system.runtime.remoting>
  <application>
   <channels>
    <channel ref="http" port="1234" />
   </channels>
   <service>
    <wellknown mode="Singleton" type="myNamespace.myClass,
     myAssembly" objectUri="myClass.soap">
   </service>
  </application>
 </system.runtime.remoting>
</configuration>

Assuming this file is saved as MyApp.exe.config, you can instantiate the remote object from the client using the following code:

C#

String filename = "MyApp.exe.config";
RemotingConfiguration.Configure(filename);
MyClass obj = new MyClass();

VB.NET

Dim filename as string
Filename = "MyApp.exe.config"
RemotingConfiguration.Configure(filename)
Dim obj as MyClass
Obj=new MyClass()

Of course, the client still requires the definition of the class MyClass in order to create an instance of the class. You could provide the implementation of MyClass to the client, but this poses code security risks and deployment problems. Neither the shared interface nor the shared base class method is suitable for the above example for providing class definitions, so in this case you should use soapsuds to generate a DLL for the client to reference in order to create these remote objects. The -nowp switch should be used with soapsuds to ensure that the DLL does not have hard-coded parameters.

In most cases, this should be all that is required to deploy a remoting service with configuration files; however, some developers may run into a problem where a remote object returns a By Value object, containing its own methods. In this case, the client must have a local reference to the By Value object, so it can deserialize the object and execute its methods. But a problem occurs because the namespace generated by soapsuds will be the same as the By Value object’s namespace. To avoid this namespace name clash, you should manually edit the soapsuds-generated proxy DLL from its source code, which can be obtained by calling soapsuds with the -gc switch. Once the C# code can be edited, the namespace can be changed to something else, thereby avoiding the namespace clash.

Hosting remote objects within IIS

Remote objects, as described thus far, have been hosted in simple Windows applications. In reality, remote object servers generally do not require a user interface, but they often require the ability to execute on a computer regardless of whether a user is actively logged in. For this reason, you will probably want to run your remote object server as a service. Chapter 10 covers this topic in more detail.

Another alternative, which may be more applicable for shared hosting environments, is to host the remote object within IIS. This can be achieved by adding a little XML to the web.config file thus:

XML

<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown
          mode = "Singleton"
          type = "RemoteObject.IDGenerator,RemotingServer"
          objectUri = "RemoteObject.soap"
        />
      </service>
    </application>
  </system.runtime.remoting>
</configuration>

Hosting remote objects within a Windows service

When an application is designed to run unattended on a computer, and has no need for a user interface, it should run as a Windows service. Windows services run in the background even when no user is currently logged on. They are controlled via Control Panel→Administrative Tools→Services, where you can start, stop, and restart the service, as well as view information about it.

It is possible to use IIS as a host for remoting objects, but if you are developing a mass-market software product, not all users have IIS on their computers, nor will they want to go to the hassle of installing it.

This example requires the client and object from the previous example, so if you have not done so, now is a good time to type it in. Start a new Windows service (not application) project in Visual Studio .NET, scroll down the code to the OnStart and OnStop methods, and add the following code:

C#

Thread thdServer;
protected override void OnStart(string[] args)
{
  thdServer = new Thread(new ThreadStart(serverThread));
  thdServer.Start();
}

VB.NET

Dim thdServer As Thread
Protected Overrides Sub OnStart(ByVal args() As String)
        thdServer = New Thread(New ThreadStart( _
                    AddressOf serverThread))
        thdServer.Start()
End Sub

The two events OnStart and OnStop are triggered whenever the service is started or stopped from Administrative Tools→Services. The above code will simply start a new thread at the serverThread function. Note that the thread variable is outside of the method call, which provides a means for OnStop to disable the service by stopping the thread.

C#

protected override void OnStop()
{
  thdServer.Abort();
}

VB.NET

Protected Overrides Sub OnStop()
  thdServer.Abort()
End Sub

ServerThread is taken verbatim from the chapter 4 example. It opens an HTTP channel on port 8085 for the RemoteObject assembly.

C#

public void serverThread()
{
  HttpChannel channel = new HttpChannel(8085);
  ChannelServices.RegisterChannel(channel);
  RemotingConfiguration.RegisterWellKnownServiceType(
      typeof(RemoteObject.IDGenerator),
      "RemotingServer",
      WellKnownObjectMode.Singleton);
}

VB.NET

Public Sub serverThread()
  Dim channel As HttpChannel = New HttpChannel(8085)
  ChannelServices.RegisterChannel(channel)
  RemotingConfiguration.RegisterWellKnownServiceType( _
    (New RemoteObject.RemoteObject.IDGenerator).GetType(), _
      "RemotingServer", _
      WellKnownObjectMode.Singleton)
End Sub

As before, the code establishes a connection channel using HTTP over port 8085. The object is hosted in singleton mode, meaning that only one instance of the object is ever created. This mode is required for this application because the object needs to maintain a private variable, which is shared between all clients that call the remote object.

Services cannot be run directly from the command line; they must be installed. To prepare a service for deployment, right-click on the service in design view and select Add Installer. Click on ServiceInstaller1, and set the ServiceName property to MyService. Set the Account property of ServiceProcessInstaller1 to LocalSystem.

Finally, you need to add three references: one to System.Configuration.Install.dll, one to System.Runtime.Remoting, and another that points at the compiled DLL for the IDGenerator assembly. Then add the required namespaces as shown:

C#

using System.Configuration.Install;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Threading;

VB.NET

Imports System.Configuration.Install
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports System.Threading

Compile the application in Visual Studio .NET. You will not be able to run it directly. You need to install the service first. To do so, you now must go to the DOS command prompt. Navigate to the path where the compiled .exe file resides, and then type the following at the command prompt:

DOS

path %path%;C:WINDOWSMicrosoft.NETFrameworkv1.0.3705
installutil service.exe
net start MyService

Note

The path C:windowsMicrosoft.NETFrameworkv1.0.3705 may differ among computers. If you have two versions of the .NET framework on your machine, use the latest version.

You may be prompted to enter a valid Windows username and password during the installation process. To test the application, open the Services snap-in at Control Panel→Administrative Tools→Services. If you scroll down the list, you should see an entry named “Service1” (Figure 17.4), which should be already started. Now open the client program that was created for the example in chapter 4. Press Get unique ID, and you should see a message box appear with a number on it.

Remoting Server running as a Windows service.

Figure 17.4. Remoting Server running as a Windows service.

Distributed garbage collection

Garbage collection occurs when an object is loaded into memory and no program holds a reference to it. In a distributed environment, it is much more difficult to monitor which programs hold references to a remote object, especially if the programs in question are hundreds of miles away over unreliable dial-up connections.

There are two ways to solve this problem: client activation and server activation. Server activation is where the client has no control over the lifetime of the object, and client activation is where the client has full control over the lifetime of the object.

When the object is created using the Activator.GetObject() command, then the object is server activated. Server activation comes in two forms: singleton and single call. Singleton activation is where one, and only one, instance of the object is created on the server. This implies that state information for the object is shared between all clients. Single-call activation is a second form of server activation in which the object is created whenever any method is called on it. The object will be destroyed again once the method call is complete. This implies that there is no state information held in the object.

Client-side activation manages objects by lifetime leases, which means that a client can instruct the server to create an object and to keep it in memory for a specified time before destroying it. Client-side activation occurs when the object is created using Activator.CreateInstance().

In order to modify the lease parameters of a remote object, you simply override the InitializeLifetimeService method and change the properties of the ILease interface (Table 17.1).

Table 17.1. Significant members of the ILease interface.

Method or Property

Purpose

InitialLeaseTime

Specifies the amount of time a remote object will stay in memory before it is garbage-collected, assuming no action was taken on the object. The default is five minutes.

CurrentLeaseTime

Represents the amount of time left before the remote object is garbage-collected.

RenewOnCallTime

Specifies the amount of extended lease time that should be added if the object is called. The default is two minutes.

SponsorshipTimeout

Indicates the amount of time the lease manager will wait for a sponsor to respond before moving to another sponsor or garbage-collecting the object. The default is two minutes.

LeaseManagerPollTime

Specifies the interval of time between scans for expired leases by the underlying lease manager process. The default is ten seconds.

C#

using System;
using System.Runtime.Remoting.Lifetime;
namespace RemoteObject
{
  public class IDGenerator : System.MarshalByRefObject
  {
    private int lastID =0;
    public override Object InitializeLifetimeService()
       {
        ILease lease =
        (ILease)base.InitializeLifetimeService();
        if (lease.CurrentState == LeaseState.Initial)
        {
          lease.InitialLeaseTime = TimeSpan.FromMinutes(5);
          lease.SponsorshipTimeout = TimeSpan.FromMinutes(6);
          lease.RenewOnCallTime =
          TimeSpan.FromSeconds(7);
        }
        return lease;
      }
      public int getID()
      {
        return(lastID++);
      }
   }
}

VB.NET

Imports System
Imports System.Runtime.Remoting.Lifetime
Namespace RemoteObject
  Public Class IDGenerator
   Inherits System.MarshalByRefObject
    Private lastID As Integer = 0

    Public Overrides Function _
    InitializeLifetimeService() As [Object]
       Dim lease As Ilease
       Lease = _
       CType(MyBase.InitializeLifetimeService(), ILease)
           If lease.CurrentState = LeaseState.Initial Then
            lease.InitialLeaseTime = TimeSpan.FromMinutes(5)
            lease.SponsorshipTimeout = _
                    TimeSpan.FromMinutes(6)
            lease.RenewOnCallTime = TimeSpan.FromSeconds(7)
       End If
       Return lease
       End Function

    Public Function getID() As Integer
    lastID = lastID + 1
    return(lastID)
    End Function
 End Class
End Namespace

Looking closely at the code above, you can see that this is the same object as described earlier in the chapter. The difference is the overridden function, which provides access to the lease parameters of the object. To obtain an interface to the lease parameters, a call is made to base.Initial-izeLifetimeService(), which returns the lease interface.

Some lease parameters cannot be changed once the object has been instantiated, thus the lease’s current state is checked with the lease.CurrentState property to ensure that the object is just in the process of being created.

The InitialLeaseTime, SponsorshipTimeout, and RenewOnCallTime properties are set to five, six, and seven minutes, respectively. This will keep the object in memory longer than the default object lifetime.

To test this code, compile the object, and run the client program as described earlier. Calls to the object will work fine for the first five minutes, but any call made more than seven minutes after the previous call will cause incorrect operation because the object will have been garbage collected.

Conclusion

Web services have been very much hyped as the next big thing in information technology. They are arguably one of the simplest remote procedure call systems ever developed and possibly the most interoperable technology ever developed by Microsoft. Having said that, remoting can outperform Web services under most conditions, and the technology is still in its infancy. Many features, especially within WSE 1.0, are underimplemented and could easily cause headaches for some developers.

As this concludes this book on .NET networking, I hope it proves beneficial to you and helps you to further your career as a professional developer. Good luck, and may your programs be bug free and efficient!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.93.141