To clarify on my question I've been developing an app that does a lot of database updates / web service calls based on the input from a user (using an excel spreadsheet). If there are a lot of updates to make the process can take in excess of 20 minutes to run.
To stop my UI from freezing / timing out I've been looking into multithreading so I can run my long running process in an asynchronous manner and in the mean time simply displaying an animated gif whilst the process runs.
This all seems to run nicely at the moment with my test data, but when I substitute in the actual long running process I get an error regarding HttpContext.Current.User.Identity.Name. I've read up on this and from this article1 I took it to mean that if you set the 'Async' property to 'true' in the page directive and used the RegisterAsyncTask method you could then access HttpContext.Current. However, for me this doesn't seem to be true. I'm sure it's something I'm doing, so here is my code (I've mainly been using the following articles to write this article2 and article3):
ASP.NET page
<%@ Page Title="Home Page" Async="true" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="false" CodeBehind="Index.aspx.cs" Inherits="MyApp.Index" %>
C# - RegisterAsyncTask is done on a button click, which starts the long running process:
protected void ProcessUpdates()
{
//Register async task to allow the processing of valid updates to occurr in the background
PageAsyncTask task = new PageAsyncTask(OnBegin, OnEnd, OnTimeOut, null);
RegisterAsyncTask(task);
}
IAsyncResult OnBegin(Object sender, EventArgs e, AsyncCallback cb, object state)
{
return Worker.BeginWork(cb, state);
}
private void OnEnd(IAsyncResult asyncResult)
{
//UpdateResults list should now have been filled and can be used to fill the datagrid
dgProcessedUpdates.DataSource = Worker.UpdateResults;
dgProcessedUpdates.CurrentPageIndex = 0;
dgProcessedUpdates.DataBind();
lblProgress.Text = "Update Results: update success / failure is shown below";
}
private void OnTimeOut(IAsyncResult asyncResult)
{
lblProgress.Text = "The process has timed out. Please check if any of the updates have been processed.";
}
C# - Worker class
public class Worker
{
public static List<AuditResult> UpdateResults = new List<AuditResult>();
private delegate void del();
//This method is called when the thread is started
public static IAsyncResult BeginWork(AsyncCallback cb, object state)
{
del processing = DoUpdateProcessing;
return processing.BeginInvoke(cb, state);
}
private static void DoUpdateProcessing()
{
//UpdateResults = ExcelFileProcessing.PassValidUpdates();
//Testing
Thread.Sleep(5000);
int i = 0;
while(i < 10)
{
AuditResult ar = new AuditResult();
ar.Result = "Successful";
ar.JobNumber = (1000 + i).ToString();
ar.NewValue = "Test New Value " + i.ToString();
ar.ResultDate = DateTime.Now.ToString();
ar.UserName = HttpContext.Current.User.Identity.Name;
UpdateResults.Add(ar);
i++;
}
}
}
Initially my test code didn't include a call to HttpContext.Current.User.Name for ar.UserName but after my issues with putting back in the call to ExcelFileProcessing.PassValidUpdates() with this I decided to do it. When I reach that part (ar.UserName = HttpContext.Current.User.Identity.Name) it says 'Object reference not set to an instance of an object', which suggests the HttpContext isn't carried across to the second thread. How can I do this?
UPDATE
I've currently reverted back to my previous code (that wasn't initially working) and simply passed the HttpContext.Current as a variable to my DoWork method as per this SO question like this:
Create 2nd thread
protected void ProcessValidUpdates()
{
Worker workerObject = new Worker();
HttpContext ctx = HttpContext.Current;
Thread workerThread = new Thread(new ThreadStart(() =>
{
HttpContext.Current = ctx;
workerObject.DoWork();
}));
workerThread.Start();
//Loop until worker thread activates
while (!workerThread.IsAlive) ;
//Put main thread to sleep to allow the worker thread to do some work
Thread.Sleep(1000);
//Request the worker thread stop itself
workerObject.RequestStop();
//Use the Join method to block the current thread until the object's thread terminates
workerThread.Join();
//UpdateResults list should now have been filled and can be used to fill the datagrid
dgProcessedUpdates.DataSource = Worker.UpdateResults;
dgProcessedUpdates.CurrentPageIndex = 0;
dgProcessedUpdates.DataBind();
lblProgress.Text = "Update Results: update success / failure is shown below";
}
Worker Class
public class Worker
{
//volatile hints to the compiler that this data member will be accessed by multiple threads.
private volatile bool _shouldStop;
public static List<AuditResult> UpdateResults = new List<AuditResult>();
//This method is called when the thread is started
public void DoWork()
{
while (!_shouldStop)
{
//Testing
Thread.Sleep(5000);
int i = 0;
while (i < 10)
{
AuditResult ar = new AuditResult();
ar.Result = "Successful";
ar.JobNumber = (1000 + i).ToString();
ar.NewValue = "Test New Value " + i.ToString();
ar.ResultDate = DateTime.Now.ToString();
ar.UserName = HttpContext.Current.User.Identity.Name;
UpdateResults.Add(ar);
i++;
}
}
}
public void RequestStop()
{
_shouldStop = true;
}
}
This seems to work in that I can now access HttpContext.Current and the username I expect. I think this is probably to some degree what some of you were proposing anyway. I appreciate the solution suggested by Andrew Morton but at the moment that would require a significant rewrite. At the moment my process already calls a web service to do the database stuff and returns a success or failure result. It also has to call another BPEL service directly. As such I suspect there may be further performance hits if I had to wrap all this into another web service. In addition, most calls to the process won't be that long running (probably less than 10 mins), so this is really only to address the few requests that exceed 20 mins. Finally, this is only likely to be used by 1 or 2 people, so it's unlikely to have a huge number of requests at 1 time.
However, bearing in mind my current solution, is there anything I should be aware of that might trip me up? IIS causing issues? Any additional help would be very much appreciated.
The static property Current on the HttpContext class can be useful whenever the flow of control leaves the code in your Page derived web form. Using this property you can reach out and magically grab the current Request, Response, Session, and Application objects (and more) for the request you are servicing.
The HttpContext is NOT thread safe, accessing it from multiple threads can result in exceptions, data corruption and generally unpredictable results. The IHttpContextAccessor interface should be used with caution.
This is useful when you have a common service that is used by your controllers. You can then access the current HTTP context in a safe way: var context = _httpContextAccessor. HttpContext; // Do something with the current HTTP context...
I have a site on a shared server. I need to have a BATCH job and I do that in another thread. It can run up to 1 hour (I ping the site so the worker process does not stop).
I went down the road of tying to get the current context. After many hours of research and searching it cannot be done. In a new thread the httpcontent.current is not there, it is not the same thread as the user was accessing, so the context did not carry over, and you cannot access the logged in user, since they are not logged into that thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With