Click here to Skip to main content
Click here to Skip to main content

Server-Side Asynchronous Methods for ASP.NET and WinFX

, 6 Apr 2006
Rate this:
Please Sign up or sign in to vote.
An article that describes how to create server-side asynchronous service methods for ASP.NET and WinFX, thereby freeing up resources on the server to handle more clients simultaenously.

Sample Image

Introduction

The purpose of this article is to demonstrate how to expose a method from an ASP.NET Web Service or WinFX Service that uses the asynchronous pattern internally, to increase the scalability of the service. Specifically, how to prevent the threads that are being used to service requests from blocking while background I/O operations take place.

Please note, this article does not deal with applications simply making asynchronous calls to Web Services, as this has been amply covered already. However, if you are familiar with that, you'll notice plenty of similarities between the client-side and the server-side implementation.

Contents

Requirements

I've written this using Visual Studio 2005, .NET 2.0, and the WinFX Feb CTP, on a Windows XP machine. The ASP.NET Web Service code should run on .NET 1.0 and .NET 1.1, although I haven't tested this.

Background

Before we start, let's just recap what normally happens when a consumer makes a request against an ASP.NET Web Service. The consumer issues a SOAP request to IIS which will hand the request over to the ASMX handler. If it hasn't already done so, the ASMX handler will build/reflect over the assemblies in the bin folder, looking for the methods decorated with the WebMethod attribute. The ASMX handler can then match a SOAP request with a specific method, and hence deserialises the request into the input parameters of that method. When that method returns, the thread running it is returned to the pool, and the associated HttpContext is released.

Writing the code - ASP.NET Web Service

Before we start writing any code to exploit the asynchronous features of the hosting platform, we need to create a synchronous method that performs some kind of I/O. I've chosen file-based I/O, but the principles apply equally to network I/O, such as a call to a backend ASP.NET Web Service or database. Below is the code for a method that opens a large file and counts the number of zero bytes inside it. I've called it SyncWork. The large file I've chosen is the TrueType Arial font because it's installed on most XP machines and weighs in at 22 MB, but any large file will do.

[WebMethod]
public Int32 SyncWork() 
{
    FileStream fs = 
      new FileStream(@"c:\windows\fonts\arialuni.ttf", 
      FileMode.Open);

    try
    {
        Byte[] buffer = new Byte[fs.Length];
        fs.Read(buffer, 0, (Int32) fs.Length);

        Int32 count = 0;
        foreach (Byte b in buffer)
        {
            if (b == 0) count++;
        }
        return count;
    }
    finally
    {
        fs.Close();
    }
}

Looking at SyncWork, we can identify that the call to FileStream.Read is going to involve reading some data from the hard disk. This is going to take some time to complete, and while we're waiting, our thread will be blocked and hence not performing any useful work. It would be better if this thread could be returned to the thread pool managed by our host (in this case IIS, or specifically the ASMX handler) so that more requests could be served. This is a scenario where implementing server-side asynchronous methods can help to make our service more scalable.

I'm going to create a new method on the service called AsyncWork which will perform the same function for the consumer as SyncWork but it will behave asynchronously. Although AsyncWork will appear to the consumer as a single method, we have to write two separate methods, named BeginAsyncWork and EndAsyncWork, respectively. The required signatures for these methods are shown below:

[WebMethod]
public IAsyncResult BeginAsyncWork(AsyncCallback callback, Object state)
{
    ...
}

[WebMethod]
public Int32 EndAsyncWork(IAsyncResult ar)
{
    ...
}

Notice that the BeginAsyncWork method has two additional input parameters, appended to the end of the parameter list. In this case, they are the only two input parameters. The first parameter refers to a callback method that should be called when the background I/O work is complete. The state object is anything that the host would like us to associate with this asynchronous work so that it can identify it later. In my experience, this has always been null, but there's nothing to suggest it won't be used in the future so it's important to treat it properly. The BeginAsyncWork method must return an IAsyncResult. This same IAsyncResult will be passed as the sole parameter to the EndAsyncWork method when the background I/O operation is complete. The EndAsyncWork method will have the same return type as the SyncWork method, in this case an Int32.

Crucially, the EndAsyncWork method may be called on a different instance of our Service class than the call to BeginAsyncWork. This is because the thread on which the BeginAsyncWork code ran will be returned to the thread pool as soon as it is finished so that it can be used to service another request - the whole point of this exercise in fact! A thread won't be selected to run the EndAsyncWork code until the ASMX handler receives notification that the background I/O operation is complete. All of this grabbing and releasing of threads means that we can't share information between the BeginAsyncWork and EndAsyncWork methods using member variables on our Service class. We'll need to create a separate class to hold state information, and give it to the ASMX handler to look after for us. Since the ASMX handler is going to get an object that implements IAsyncResult from BeginAsyncWork, and it's going to pass this object to EndAsyncWork, it makes sense for our state to be contained in that object. So in short, we'll store our state information in the object that implements IAsyncResult. The sequence diagram below shows the messages that are sent between the various objects during the process:

The consumer issues an AsyncWork request, which causes the ASMX handler to call BeginAsyncWork, which creates a state object and starts the background I/O operation via the FileStream's BeginRead method. The service1 object is finished so it can be garbage collected and its thread returned to the pool. When the background I/O operation finishes, it calls the ReadOperationComplete method on the serviceState object which in turn invokes the ASMX handler's callback method. The ASMX handler responds to this notification by calling the EndAysncWork method which finishes the job. Notice in the diagram above that the two Service classes, service1 and service2, have short life-times with respect to the whole operation (as shown in yellow).

A state class needs to maintain several pieces of information. It must know about the callback and state that was passed into the BeginAsyncWork method. It must reference the variables shared between the BeginAsyncWork and EndAsyncWork methods, which in this case boils down to a FileStream, an IAsyncResult returned from the FileStream, and a Byte array. It must implement the IAsyncResult interface so that it can be returned to the ASMX handler. Finally, it must provide a method which the background operation (FileStream, in this case) can call to tell us that it's finished. The ServiceState class below satisfies these requirements:

public class ServiceState : IAsyncResult
{
    // callback and state object for the host
    public AsyncCallback HostCallback;
    public Object HostState;

    // information needed for the background I/O call
    public Byte[] Buffer;
    public FileStream Stream;
    public IAsyncResult ReadOperationAsyncResult;

    // implementation of the IAsyncResult interface for the host
    public object AsyncState { get { return HostState; } }
    public WaitHandle AsyncWaitHandle { get { 
           return ReadOperationAsyncResult.AsyncWaitHandle; } }
    public Boolean CompletedSynchronously { get { 
           return ReadOperationAsyncResult.CompletedSynchronously; } }
    public Boolean IsCompleted { get { 
           return ReadOperationAsyncResult.IsCompleted; } }

    // our callback method that will be
    // notified when the background I/O is done
    public void ReadOperationComplete(IAsyncResult ar)
    {
        ServiceState serviceState = ar.AsyncState as ServiceState;
        serviceState.HostCallback(serviceState);
    }
}

Notice that this implementation of the IAsyncResult interface is achieved by simply wrapping most of the properties of the ReadOperationAsyncResult member. This member is provided by the FileStream object when calling BeginRead, and provides information about that specific background operation. Since this is our only background operation, we can just wrap it. If we had multiple background operations, we would need to implement IAsyncResult ourselves with properties that reflected the complete picture. For example, if only one of two background operations had completed, then the IsCompleted property would need to return false. The AsyncState property returns the HostState object. This is because if the host was to access the AsyncState property, then it would expect to get the same object that it provided in the initial call to BeginAsyncWork.

So, let's actually write the BeginAsyncWork method. We begin by constructing the object that will contain our state information, a ServiceState object in this case. I've stored the callback and the state. I've then created the FileStream, and subsequently the Byte array, that I need for the operation, and attached these to the state also. I then start the background I/O operation by providing the input parameters for both the read operations (a Byte array, start point, number of bytes) and for the asynchronous facility (a callback and my state object). My state object, serviceState, is then returned as this implements the IAsyncResult interface and allows the host to keep track of the background operation.

[WebMethod]
public IAsyncResult BeginAsyncWork(AsyncCallback callback, Object state)
{
    ServiceState serviceState = new ServiceState();
    serviceState.HostCallback = callback;
    serviceState.HostState = state;
    serviceState.Stream = new 
      FileStream(@"c:\windows\fonts\arialuni.ttf", FileMode.Open);

    try
    {
        serviceState.Buffer = new Byte[serviceState.Stream.Length];
        serviceState.ReadOperationAsyncResult = 
             serviceState.Stream.BeginRead(serviceState.Buffer, 0, 
            (Int32)serviceState.Stream.Length, 
             new AsyncCallback(serviceState.ReadOperationComplete), serviceState);
        return serviceState;
    }
    catch (IOException)
    {
        serviceState.Stream.Close();
        throw;
    }
}

When the background I/O operation is complete, it will call back to the ServiceState.ReadOperationComplete method. Taking a look at that method again below, you can see that I'm extracting the serviceState object that I provided, and I'm using it to access the HostCallback. By invoking this method, I'm letting the ASMX handler know that the job is done and that the EndAsyncWork method should now be called.

public void ReadOperationComplete(IAsyncResult ar)
{
    ServiceState serviceState = ar.AsyncState as ServiceState;
    serviceState.HostCallback(serviceState);
}

The ASMX handler will call EndAsyncWork, and this is where the original request made by one of the consumers of our service should be fulfilled. The method below shows how this is done. First, I extract the serviceState, and then call the FileStream.EndRead method to complete the Read operation. Finally, the serviceState object is accessed again in order to work on the Byte buffer that has been populated so as to determine the number of zero bytes. This value is returned from the method, and hence to the consumer, and this request has now been fulfilled.

[WebMethod]
public Int32 EndAsyncWork(IAsyncResult ar)
{
    ServiceState serviceState = (ServiceState)ar;
    try
    {
        serviceState.Stream.EndRead(serviceState.ReadOperationAsyncResult);
    }
    finally
    {
        serviceState.Stream.Close();
    }

    Int32 count = 0;
    foreach (Byte b in serviceState.Buffer)
    {
        if (b == 0) count++;
    }
    return count;
}

Writing the code - WinFX Service

To get this working under WinFX requires a few very minor modifications. Rather than creating a new project and a new class, I'm going to convert the existing class because it's quicker. I'm using the Feb CTP build for this. First, we need to define a contract for our service, which is done by simply defining an interface decorated with the ServiceContract attribute. The interface for our service is shown below. I've called it IWinfxService.

[ServiceContract()]
public interface IWinfxService
{
    [OperationContract]
    Int32 SyncWork();

    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginAsyncWork(AsyncCallback callback, Object state);

    Int32 EndAsyncWork(IAsyncResult ar);
}

The first method in our contract is the SyncWork method. As you might expect, it has the same signature as the SyncWork method defined on the Service class, but it is also decorated with the OperationContract attribute. For methods that the WinFX host (in this case, still IIS) is to treat asynchronously, we need to set the AsyncPattern property of the OperationContract to true. As a result of doing this, we no longer need to decorate the EndAsyncWork method with any attributes but it does need to be present in the interface.

Next, we need to indicate that our Service class implements this interface. We do this by appending IWinfxService to the class declaration.

public class Service : System.Web.Services.WebService, IWinfxService
{
    ...
}

Next, we need an entry point for our WinFX host, which in this case is IIS. To do this, we create a Service.svc file in the root of the website and put in the line of text shown below. This line instructs the WinFX host to look for a class called Service in a file called ~/App_Code/Service.cs. If you create your WinFX services using the 'Add New Item' dialog, then this will normally be created for you, but here we're converting an ASP.NET Web Service so we have to create it manually.

<% @ServiceHost Language="C#" Debug="true" 
        Service="Service" CodeBehind="~/App_Code/Service.cs" %>

Finally, we need to put a few extra lines into the Web.Config file. Again, if you had used Visual Studio and the 'Add New Item' dialog to create your WinFX service, then this would be done for you. The section that needs to be added to the Configuration node is shown below. Please bear in mind that this is one possible configuration - you can change the binding used for the service, and you would certainly want to change the behaviour such that it didn't return detailed faults to the consumer.

<system.serviceModel>
    <services>
        <service name="Service" behaviorConfiguration="returnFaults">
            <endpoint contract="IWinfxService" binding="wsHttpBinding"/>
        </service>
    </services>
    <behaviors>
        <behavior name="returnFaults" 
                 returnUnknownExceptionsAsFaults="true" />
    </behaviors>
</system.serviceModel>

Now, we just need to test it. First, check that the WinFX service builds OK by setting the website as the startup project and running it. When the IE window pops up, navigate to the service.svc page. Highlight the full url, which will be something like http://localhost:1234/WebSite2/Service.svc, and press Ctrl-C. Next, create a new console application. Then, right-click on the project and select 'Add Service Reference'. In the dialog that appears, paste the URL into the top box and type 'localhost' in the bottom box, and click OK. This sets up a reference to your service. In the Feb CTP of WinFX, there's no support for browsing to a WinFX service in your solution but this will almost certainly improve in newer builds.

Finally, replace the auto-generated Program class with the one below to test your WinFX service. You can use break-points to satisfy yourself that the service is using the asynchronous pattern internally.

class Program
{
    static void Main(string[] args)
    {
        localhost.WinfxServiceProxy proxy = 
           new localhost.WinfxServiceProxy();
        Console.WriteLine(proxy.AsyncWork());
        Console.ReadKey();
    }
}

If you have any problems running the ConsoleApplication test harness included in the download, then it's probably because, on your computer, the website has been assigned a different port number. One way to fix this is to just remove and re-add the Service Reference using the instructions explained two paragraphs above.

Summary

I've shown how to create a service method using the asynchronous pattern to increase the scalability of services that need to perform background I/O operations whether that be implemented using an ASP.NET Web Service or a WinFX service.

History

  • No changes made so far.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Karl M. Hulme
Web Developer
United Kingdom United Kingdom
I live with my wife in Bournemouth in the UK. She consistently inspires me to be the person I know I should be. I enjoy writing music, playing the piano, watching movies, philisophical discussion and eating jaffa cakes.

Comments and Discussions

 
Questionsystem.serviceModel won't let me compile Pinmemberkazoo of the north23-Oct-07 11:15 
AnswerRe: system.serviceModel won't let me compile Pinmemberkazoo of the north23-Oct-07 11:25 
GeneralRe: Karl please help - .Net v1.1 Pinmemberaks776724-Oct-06 8:29 
Karl:
 
I hope you got the opportunity to work on this issue, am awaiting your solution with the modifications to the code for .Net v 1.1.
 

Akshay
AnswerRe: Karl please help - .Net v1.1 PinmemberKarl M. Hulme25-Oct-06 23:10 
GeneralRe: Object Reference issue Pinmemberaks776712-Oct-06 7:09 
GeneralRe: Object Reference issue PinmemberKarl M. Hulme16-Oct-06 5:58 
GeneralRe: Object Reference issue Pinmemberaks776716-Oct-06 7:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 6 Apr 2006
Article Copyright 2006 by Karl M. Hulme
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid