Reputation: 4253
I'm working on a legacy webforms app, Framework 4.8. Some of the code I'm interacting with is async. It works with NHibernate, whose session is stored in HttpContext for the request. The problem is I'm losing access to the context in some async work, which means I cannot access the current session from that point on.
Here's a simple example:
public partial class Test : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
// I exist, obviously
var a = System.Web.HttpContext.Current;
var asyncTask = new System.Web.UI.PageAsyncTask(() => {
// I still exist here too
var b = System.Web.HttpContext.Current;
return Task.Run(() => {
// I'm null here - oh noes!
var c = System.Web.HttpContext.Current;
// Pretend to do some work
return Task.Delay(5000);
});
});
RegisterAsyncTask(asyncTask);
}
}
HttpContext.Current
is null while inside the task.
I'm using NHibernate's WebSessionContext
, which uses HttpContext
to store the request's current session. I have async code that's using NHibernate's ISessionFactory
to fetch the current request's session, except that if the executing code needs a session and while doing actual asynchronous work, we end up with a different session than the current request's, since its using HttpContext and can no longer find the original context. (As illustrated in the example above.)
I'm target framework 4.8 in my web.config:
<httpRuntime targetFramework="4.8">
[snip...]
and have a setting defined for
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
I realize that, ideally, it would be easiest to get the session at the start of the request and pass it around. Unfortunately, that is not currently possible everywhere without a significant amount of work.
Is there any way to maintain access to the context?
Upvotes: 1
Views: 649
Reputation: 48989
You not going to gain anything at all here, since if you fire off some task, but wait for it to complete, then the web page is still stuck up on the server, and thus the user going to still see the browser 'spinner'.
And if you allow the page to complete (which means ALL running code for that page class, and THEN the browser page now travels back to client. Now, your page class is disposed, goes out of scope, and hte web server just sitting waiting for ANY of the users to post back a page.
Once that page is sent back to browser side, on server side, that page class and web page is dumped from memory - it does not exist anymore.
If you want some button click to start a process?
Then use a web method call, and no page waiting will occur.
You also don't say how long this process is.
I mean, say we have 5 steps. We want to have progress bar of the 5 steps.
We have two choices:
Buttion click, call server side code, (ajax), and we wait for the method to complete, then update page, and then call step 2.
You can also of course say have one routine, pass it a "step" and then when done HAVE THE CLIENT SIDE start the next step.
And, since say we have 5 steps, lets drop the whole mess inside of a update panel so we don't see post-backs.
So, this markup:
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Button ID="cmdProcess" runat="server" Text="to hide button" ClientIDMode="Static"
OnClick="cmdProcess_Click" style="display:none" />
<asp:Button ID="Button1" runat="server" Text="Start the Reactor"
ClientIDMode="Static" CssClass="btn"
OnClientClick="startprocessor('Start processing','1','4');return false"
/>
<br />
<asp:HiddenField ID="pStep" runat="server" ClientIDMode="Static" />
<asp:HiddenField ID="pSteps" runat="server" ClientIDMode="Static" />
<div id ="MessageArea" style="display:none">
<div id="pbar" style="width:400px;background-color:lightskyblue"></div>
<asp:Label ID="lblmsg" runat="server" Text="" ClientIDMode="Static"></asp:Label>
<br />
</div>
</ContentTemplate>
</asp:UpdatePanel>
So, note show we have a hidden button.
I also added pStep (the processor step), and pSteps.
So, we could have say Working on step 1 of 5. (but, I don't use it just yet in this example).
So, so when we start the process, I use a client side click, pass the number of steps and the FIRST starting message.
Also, since I not using a ajax call, I had to google, and it turns out there is a event when the update panel is done.
So, right below above markup, I have this client side code:
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(EndRequestH);
function EndRequestH(sender, args) {
// code here
lbl = $('#lblmsg')
if (lbl.text() !== "") {
// more processing required
$('#MessageArea').show()
iStep = $('#pStep').val()
iSteps = $('#pSteps').val()
$('#pbar').progressbar({ value: iStep / iSteps * 100 });
$('#cmdProcess').click()
}
}
function startprocessor(sMsg,iStep,iSteps) {
$('#lblmsg').text(sMsg)
$('#pStep').val(iStep)
$('#pSteps').val(iSteps)
EndRequestH()
}
</script>
And now my server side event is this "fake" processing steps:
protected void cmdProcess_Click(object sender, EventArgs e)
{
string Step = pStep.Value;
switch (Step)
{
case "1":
// code here for step one
Thread.Sleep(2000);
// if more steps, then setup next message
pStep.Value = "2";
lblmsg.Text = "Sending emails...";
return;
case "2":
// code here for step 2 -- send emails
Thread.Sleep(3000);
pStep.Value = "3";
lblmsg.Text = "Building Projects for customers...";
return;
case "3":
// code here for step 3 - Building project for custoemrs
Thread.Sleep(2000);
pStep.Value = "4";
lblmsg.Text = "Taking pet dog for a walk..";
return;
case "4":
// code here for steop 4 - taking dog for a walk
Thread.Sleep(2000);
// last step - clear out lbl msg to STOP processing
pStep.Value = "0";
lblmsg.Text = ""; // blank message - stop processor
return;
}
}
the process loop STOPS when we setup a blank message.
When I run the above, we thus see this:
So, either you make a ajax (or page method) call from the markup, but any kind of "async" server side code with a async wait will defeat the WHOLE purpose, since the instant you wait, that is the SAME instant the web page stays up on the server. If you don't wait, then the page class is blowen out, disposed the instant the page is sent back to the client side.
Now, in place of a async task (server side), you certainly can create and call a REAL new 100% separate thread in the code behind, and then of course let the web page travel back to client, and of course then the server side page class goes out of scope and get disposed as it always will and does. At that point, you can use a javascript timer, and repeated call the server to poll the separate task you are running. That page method will however need some means to "know" that the server side process is done. This means you need to use some row in a database, or even perhaps a "flag" value you set in session.
Upvotes: 2