By now you should all know that you should never use long-running code in a WebService, as ASP.Net will simply run out of worker threads.
However, another challenge remains, how do we allow clients to receive events without having to resort to polling (which I think is a really BAD practice).
I read about how one could do this over at Udi Dahan's blog, but this seems like a bad practice to me, most importantly - it uses polling. Most people seem to forget that bandwidth can get expensive these days.
I further appropriated his method and used Asyncronous HTTP handlers to allow the client to resume when the request was done. I am not going to explain the code, as it is short and sweet.
Testing
If you want to test the code you can use the browser, if you can't figure out how to use it maybe you should find another hobby/job ;).
AsyncWebServiceResult.cs
#region Using Statements
using System;
using System.Collections.Generic;
using System.Threading;
using System.Web;
#endregion Using Statements
namespace WebService1
{
public class AsyncWebServiceResult : IAsyncResult
{
#region Fields
private static Dictionary<string, AsyncWebServiceResult> _workers = new Dictionary<string, AsyncWebServiceResult>();
private static ReaderWriterLockSlim _workersLock = new ReaderWriterLockSlim();
private AsyncCallback _callback;
private bool _completed;
private HttpContext _context;
private Exception _ex;
private string _id;
private Func<Object> _method;
private Object _return;
private bool _started;
private object _state;
#endregion Fields
#region Constructors
public AsyncWebServiceResult(Func<Object> worker, string id)
{
_method = worker;
_id = id;
} // AsyncWebServiceResult
#endregion Constructors
#region Properties
public object AsyncState
{
get { return _state; }
} // object AsyncState
public System.Threading.WaitHandle AsyncWaitHandle
{
get { return null; }
} // System.Threading.WaitHandle AsyncWaitHandle
public bool CompletedSynchronously
{
get { return false; }
} // bool CompletedSynchronously
public bool IsCompleted
{
get { return _completed; }
} // bool IsCompleted
#endregion Properties
#region Methods
public static string Create(Func<Object> worker)
{
try
{
_workersLock.EnterWriteLock();
string id = Guid.NewGuid().ToString();
AsyncWebServiceResult res = new AsyncWebServiceResult(worker, id);
_workers.Add(id, res);
return id;
}
finally
{
_workersLock.ExitWriteLock();
}
} // string Create
public static Object End(string id)
{
try
{
_workersLock.EnterWriteLock();
AsyncWebServiceResult val;
if (!_workers.TryGetValue(id, out val))
throw new InvalidOperationException();
if (!val.IsCompleted)
throw new InvalidOperationException();
_workers.Remove(id);
if (val._ex != null)
throw val._ex;
return val._return;
}
finally
{
_workersLock.ExitWriteLock();
}
} // Object End
internal static IAsyncResult Start(string id, AsyncCallback callback, HttpContext context, object extraData)
{
try
{
_workersLock.EnterReadLock();
AsyncWebServiceResult val;
if (!_workers.TryGetValue(id, out val))
throw new InvalidOperationException();
val.StartWorker(id, callback, context, extraData);
return val;
}
finally
{
_workersLock.ExitReadLock();
}
} // IAsyncResult Start
private void Complete()
{
_completed = true;
if (_callback != null)
_callback(this);
} // void Complete
private void StartWorker(string id, AsyncCallback callback, HttpContext context, object state)
{
if (_started)
return;
_started = true;
_callback = callback;
_state = state;
_context = context;
ThreadPool.QueueUserWorkItem(new WaitCallback(StartWorker), null);
} // void StartWorker
private void StartWorker(object state)
{
try
{
_return = _method();
}
catch(Exception e)
{
_ex = e;
}
Complete();
} // void StartWorker
#endregion Methods
} // Class AsyncWebServiceResult
}
wswait.ashx
#region Using Statements
using System;
using System.Web;
#endregion Using Statements
namespace WebService1
{
public class wswait : IHttpAsyncHandler
{
#region Properties
public bool IsReusable
{
get { return false; }
} // bool IsReusable
#endregion Properties
#region Methods
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
string id = context.Request.Params["id"];
if (id == null)
throw new InvalidOperationException();
return AsyncWebServiceResult.Start(id, cb, context, extraData);
} // IAsyncResult BeginProcessRequest
public void EndProcessRequest(IAsyncResult result)
{
}
public void ProcessRequest(HttpContext context)
{
throw new InvalidOperationException();
} // void ProcessRequest
#endregion Methods
} // Class wswait
}
LongService.asmx
#region Using Statements
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
#endregion Using Statements
namespace WebService1
{
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
// [System.Web.Script.Services.ScriptService]
///
/// Summary description for LongService
///
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class LongService : System.Web.Services.WebService
{
#region Methods
[WebMethod]
public string HelloWorldEnd(string id)
{
return (string)AsyncWebServiceResult.End(id);
} // string HelloWorldEnd
[WebMethod]
public string HelloWorldStart(string val)
{
return string.Format("wswait.ashx?id={0}", AsyncWebServiceResult.Create(() => Work(val)));
} // string HelloWorldStart
private object Work(string val)
{
Thread.Sleep(5000);
return "Hello " + val;
} // object Work
#endregion Methods
} // Class LongService
}