Using an IHttpAsyncHandler to call a WebService Asynchronously
Asked Answered
O

1

6

Here's the basic setup. We have an ASP.Net WebForms application with a page that has a Flash application that needs to access an external Web Service. Due to (security I presume) limitations in Flash (don't ask me, I'm not a Flash expert at all), we can't connect to the Web Service directly from Flash. The work around is to create a proxy in ASP.Net that the Flash application will call, which will in turn call the WebService and forward the results back to the Flash application.

The WebSite has very high traffic though, and the issue is, if the Web Service hangs at all, then the ASP.Net request threads will start backing up which could lead to serious thread starvation. In order to get around that, I've decided to use an IHttpAsyncHandler which was designed for this exact purpose. In it, I'll use a WebClient to asynchronously call the Web Service and the forward the response back. There are very few samples on the net on how to correctly use the IHttpAsyncHandler, so I just want to make sure I'm not doing it wrong. I'm basing my useage on the example show here: http://msdn.microsoft.com/en-us/library/ms227433.aspx

Here's my code:

internal class AsynchOperation : IAsyncResult
{
    private bool _completed;
    private Object _state;
    private AsyncCallback _callback;
    private readonly HttpContext _context;

    bool IAsyncResult.IsCompleted { get { return _completed; } }
    WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
    Object IAsyncResult.AsyncState { get { return _state; } }
    bool IAsyncResult.CompletedSynchronously { get { return false; } }

    public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
    {
        _callback = callback;
        _context = context;
        _state = state;
        _completed = false;
    }

    public void StartAsyncWork()
    {
        using (var client = new WebClient())
        {
            var url = "url_web_service_url";
            client.DownloadDataCompleted += (o, e) =>
            {
                if (!e.Cancelled && e.Error == null)
                {
                    _context.Response.ContentType = "text/xml";
                    _context.Response.OutputStream.Write(e.Result, 0, e.Result.Length);
                }
                _completed = true;
                _callback(this);
            };
            client.DownloadDataAsync(new Uri(url));
        }
    }
}

public class MyAsyncHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        var asynch = new AsynchOperation(cb, context, extraData);
        asynch.StartAsyncWork();
        return asynch;
    }

    public void EndProcessRequest(IAsyncResult result)
    {
    }

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
    }
}

Now this all works, and I THINK it should do the trick, but I'm not 100% sure. Also, creating my own IAsyncResult seems a bit overkill, I'm just wondering if there's a way I can leverage the IAsyncResult returned from Delegate.BeginInvoke, or maybe something else. Any feedback welcome. Thanks!!

Outwork answered 17/6, 2011 at 16:28 Comment(0)
W
8

Wow, yeah you can make this a lot easier/cleaner if you're on .NET 4.0 by leveraging the Task Parallel Library. Check it:

public class MyAsyncHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
    {
        // NOTE: the result of this operation is void, but TCS requires some data type so we just use bool
        TaskCompletionSource<bool> webClientDownloadCompletionSource = new TaskCompletionSource<bool>();

        WebClient webClient = new WebClient())
        HttpContext currentHttpContext = HttpContext.Current;

        // Setup the download completed event handler
        client.DownloadDataCompleted += (o, e) =>
        {
            if(e.Cancelled)
            {
                // If it was canceled, signal the TCS is cacnceled
                // NOTE: probably don't need this since you have nothing canceling the operation anyway
                webClientDownloadCompletionSource.SetCanceled();
            }
            else if(e.Error != null)
            {
                // If there was an exception, signal the TCS with the exception
                webClientDownloadCompletionSource.SetException(e.Error);
            }
            else
            {
                // Success, write the response
                currentHttpContext.Response.ContentType = "text/xml";
                currentHttpContext.Response.OutputStream.Write(e.Result, 0, e.Result.Length);

                // Signal the TCS that were done (we don't actually look at the bool result, but it's needed)
                taskCompletionSource.SetResult(true);
            }
        };

        string url = "url_web_service_url";

        // Kick off the download immediately
        client.DownloadDataAsync(new Uri(url));

        // Get the TCS's task so that we can append some continuations
        Task webClientDownloadTask = webClientDownloadCompletionSource.Task;

        // Always dispose of the client once the work is completed
        webClientDownloadTask.ContinueWith(
            _ =>
            {
                client.Dispose();
            },
            TaskContinuationOptions.ExecuteSynchronously);

        // If there was a callback passed in, we need to invoke it after the download work has completed
        if(cb != null)
        {
            webClientDownloadTask.ContinueWith(
               webClientDownloadAntecedent =>
               {
                   cb(webClientDownloadAntecedent);
               },
               TaskContinuationOptions.ExecuteSynchronously);
         }

        // Return the TCS's Task as the IAsyncResult
        return webClientDownloadTask;
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        // Unwrap the task and wait on it which will propagate any exceptions that might have occurred
        ((Task)result).Wait();
    }

    public bool IsReusable
    {
        get 
        { 
            return true; // why not return true here? you have no state, it's easily reusable!
        }
    }

    public void ProcessRequest(HttpContext context)
    {
    }
}
Wurtz answered 17/6, 2011 at 17:15 Comment(9)
I just learned a ton about the TPL thanks to your post. I like this version better, thanks!Outwork
The TPL is a scalability freaks best friend. ;) Only thing better is going to be when .NET vNext comes out with the async language extensions and you don't need to write all these crazy closures and continuations yourself. Happy coding!Wurtz
@DrewMarsh : I think that the thread that is used for asynchronous operation is the thread from the asp.net thread pool. I would like to use a background thread instead (Thread.Start). How it could be achieved with the Tasks or async/await ?Camara
It's not. The DownloadDataAaync uses I/O threads and then the event is fired on the synchronization context thread. This is because WebClient was designed for use in WinForms initially and wanted to make life easy for marshaling events back to the caller. The other thing you can do is use HttpWebRequest and use its async BeginGetResponse and do async reading from the stream yourself in .NET 4.0. This approach will never use the sync context again. To use await you need .NET 4.5 APIs and could even use the new WCF Web API's HttpClient class.Wurtz
I wrote an async version of this sample here. @Drew: SyncContext in ASP.NET is the request context; it doesn't have a particular thread (see my MSDN article) - so WebClient in WinForms marshals its events to the UI but WebClient in ASP.NET marshals its events to the request context. It's not a UI-specific type.Width
Sure, I was simply saying that WebClient was designed to use the synccontext and if that's something you don't need (which, it's not since you can always capture the ambient HttpContext in continuation closure if you need it) then it's probably better to just use HttpWebRequest. I personally never use WebClient in server scenarios. There's this reason and then I find event async pattern is more of a pain in the butt and more overhead because of event hooking than the standard aysnc pattern provided by HttpWebRequest API.Wurtz
we don't need to show any message when executing function EndProcessRequest ?Withhold
Can I please reference you to my question regarding same topic ? #15585844Toneless
Not so fluent in C# yet I believe that you ought to rename webClient to client in your example. Now there are kinda both.Dekker

© 2022 - 2024 — McMap. All rights reserved.