0

We are developing a monolithic web application – very stateful. It handles both HTTP requests and long lived SignalR connections. (In ASP.NET Core 3.1 – we will upgrade to .NET 5 later.)

We do a redirect from a login page to our “main page”. The main page takes a while to load and initialize, after that it connects with SignalR. We also have a lot of work to do at the server side. Doing the server work in the login request (before redirecting to the main page) would slow down the login.

“Oh, let’s use a Task then!”, I thought. That is, put the server work in a Task, save that in the user state, and let it execute in parallel with the loading of the main page. Something like this (simplified):

public static async Task ServerSideInit() { // do a lot of init work } // at the end of the controller handling the login page POST: UserState.BackgroundTask = ServerSideInit(); Redirect(UrlToTheMainPage); // when the main page connects via SignalR: try { await UserState.BackgroundTask; } catch { // handle errors in the init work } 

This would really speed things up. It won’t matter if the page loading or the init work finishes first – we await the Task. And the work in ServerSideInit() isn’t critical. If something happens and the main page never connects, the UserState (and the Task) will be destroyed after a timeout – and that’s perfectly OK. (There are some caveats. We would e.g. have to use IServiceProvider to create/dispose a scope in ServerSideInit(), so we get a scoped DbContext outside of the controller. But that’s OK.)

But then I read that there is a risk the ASP.NET Core framework shuts down the Task when wrapping up the POST request! (Do you have to await async methods?) The simple HostingEnvironment.QueueBackgroundWorkItem isn’t available any longer. There is a new BackgroundService class, though. (https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio) But registering a service and queueing jobs seems like a very cumbersome solution… We just want to fire a task that will take a couple of seconds to complete, and let that continue to run after ASP.NET Core has finished handling the POST request.

I’m not very experienced with ASP.NET Core… So I’d be very grateful for some input! Will my simple solution not work? Will the task be terminated by the framework? Is there some easier way to tell the framework “please don’t touch this Task”? Or is BackgroundService the way to go?

10
  • 1
    Is that task a static or dynamic? Means, will it be same for all users or different? Commented Feb 19, 2021 at 10:47
  • Once you redirect you return HTTP response nd the server will stop processing. So what your doing doesn't make a lot of sense. If you want a "fire and forget" you should use Task.Run() to spawn a new thread, not async Commented Feb 19, 2021 at 10:49
  • Does this answer your question? Simplest way to do a fire and forget method in c# 4.0 Commented Feb 19, 2021 at 10:49
  • "execute in parallel" async and parallel are different things Commented Feb 19, 2021 at 10:50
  • 1
    Here is a must-read article about this subject: Fire and Forget on ASP.NET Commented Feb 19, 2021 at 11:14

1 Answer 1

3

Doing the server work in the login request (before redirecting to the main page) would slow down the login. “Oh, let’s use a Task then!”, I thought. That is, put the server work in a Task, save that in the user state, and let it execute in parallel with the loading of the main page.

So, you have a need for request-extrinsic work. I.e., work that your server does that is outside the scope of a request.

The first question you need to ask yourself is "does this work need to be done?" In other words, "am I OK with occasionally losing work?". If this work must be done for correctness reasons, then there is only one real solution: asynchronous messaging. If you're OK with occasionally losing work (e.g., if the main page will detect that the ServerSideInit is not done and will do it at that time), then what you're really talking about is a cache, and that's fine to have an in-memory solution for.

But then I read that there is a risk the ASP.NET Core framework shuts down the Task when wrapping up the POST request!

The first thing to recognize is that shutdowns are normal. Rolling updates during regular deployments, OS patches, etc... Your web server will voluntarily shut down sooner or later, and any code that assumes it will run forever is inherently buggy.

ASP.NET Core by default will consider itself "safe to shut down" when all requests have been responded to. This is the reasonable behavior for any HTTP service, and this logic extends to every HTTP framework, regardless of language or runtime. However, this is clearly a problem for request-extrinsic code.

So, if your code just starts a task by calling the method directly (or by Task.Run, another sadly popular option), then it is living dangerously: ASP.NET has no idea that request-extrinsic code even exists, and will happily exit when requested, abruptly terminating that code.

There are stopgap solutions like HostingEnvironment.QueueBackgroundWorkItem (pre-Core) and IHostedService / IHostApplicationLifetime (Core). These register the request-extrinsic code so that ASP.NET is aware of it, and will not shut down until that code completes. However, those solutions only go partway; since they are in-memory, they are also dangerous: ASP.NET is now aware of the request-extrinsic code, but HTTP proxies, load balancers, and deployment scripts are not.

Is there some easier way to tell the framework “please don’t touch this Task”?

Back to the question at the beginning of this answer: "does this work need to be done?"

If it's just an optimization and doesn't need to be done, then just firing off the work with a Task.Run (or IHostedService) should be sufficient. I wouldn't keep it in UserState, though, since Tasks aren't serializable.

If the work needs to be done, then build an asynchronous messaging solution.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your kind and extensive answer! :-) Yes, I read up on IHostedService and such before posting my question. This is just an optimization. First, I have a login HTTP POST request, then our SPA connects via SignalR. The SPA page takes a while to load, and we also must do some other time consuming setup - by starting a Task in the POST request and await in in the SignalR OnConnectedAsync, these two things could be done in parallel. But the link I posted ...
... made me uncertain - it said SynchronizationContext keeps track on the Tasks and throws an exception when the controller action finishes. But that doesn't seem to apply to Core - I've had time to test it now, and it works fine! :-)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.