1

I am using ASP.NET Core 3.1, and I need to find a way to run a background task/thread/timer which is not connected to the current HttpContext.

For instance, consider the following code:

public class MyController : ApiControllerBase
{
    private readonly IHttpContextAccessor _contextAccessor;

    public MyController(IHttpContextAccessor contextAccessor)
    {
        this._contextAccessor = contextAccessor;
    }

    [HttpGet]
    public IActionResult DoSomething()
    {
        // Start a task on another thread.
        Task.Run(this.TaskAction);

        // Start a background thread.
        var thread = new Thread(this.ThreadAction);
        thread.IsBackground = true;
        thread.Start();
        thread.Join();

        // Start a timer.
        var timer = new System.Timers.Timer();
        timer.Interval = 1;
        timer.Elapsed += this.Timer_Elapsed;
        timer.AutoReset = false;
        timer.Start();

        Thread.Sleep(3000);

        return this.StatusCode(200);
    }

    private void TaskAction()
    {
        if (this._contextAccessor.HttpContext != null)
            Debug.WriteLine("Oh no! We have an HttpContext in the task action");
    }

    private void ThreadAction() {
        if (this._contextAccessor.HttpContext != null)
            Debug.WriteLine("Oh no! We have an HttpContext in the thread action");
    }

    private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        if (this._contextAccessor.HttpContext != null)
            Debug.WriteLine("Oh no! We have an HttpContext in the timer action");
    }
}

My problem is that in each of these background operations, the HttpContext is there. However, in certain cases (because the background operations are long-running tasks and due to the current application architecture), we must have no context in place in the new threads. That's the way things worked in .NET Framework.

My question is: what is the correct way to accomplish this? I want all my background operations to have NO HTTP context.

The only way I've found at the moment is to temporarily suspend the thread execution context flow when creating the background task. For example:

// Start a task on another thread.
using (var flowContext = ExecutionContext.SuppressFlow())
{
  Task.Run(this.TaskAction);
}

Is this ok, or am I going to screw myself over somehow?

RobSiklos
  • 8,348
  • 5
  • 47
  • 77
  • Why do you care about the HttpContext? If you don't want it, don't access it. Are you trying to solve some other problem and assume HttpContext is somehow related? Besides, it was .NET Old that kept a static HttpContext all over the place. In .NET Core you have to ask for it explicitly. The `HttpContext` you use in the controller is an instance property, not a static object – Panagiotis Kanavos Dec 14 '20 at 16:12
  • In any case, `DoSomething` is buggy. All options, `Task.Run`, raw thread and timer, were unsafe in .NET Old too. All could result in unexpected errors as the request would terminate before either of those had a chance to complete. All of them (especially the timer) could easily result in `ObjectDisposedException` exceptions or NREs, as even the Controller would be garbage-collected once a request completed – Panagiotis Kanavos Dec 14 '20 at 16:14
  • 1
    I suspect what you need is a [BackgroundService](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&tabs=visual-studio) - something that starts when the host starts and isn't related to controllers and requests at all – Panagiotis Kanavos Dec 14 '20 at 16:19
  • @PanagiotisKanavos Yes - at the end of the day, I think you're right - BackgroundService is probably what I need. Regarding your other comments, the reason is that some of the code called by the background threads behaves differently depending on whether an `HttpContext` is available. I realize that DoSomething is buggy, for the reasons you mentioned, but this is only sample code meant to illustrate my problem. – RobSiklos Dec 14 '20 at 16:28
  • What *is* the problem? This looks more like a code/inversion-of-control problem. For some reason the unspecified background job got an HttpContextAccessor when it really needed an HttpContext. Or two different methods were merged into one, depending on global state to control their behavior – Panagiotis Kanavos Dec 14 '20 at 17:30
  • @PanagiotisKanavos We have our own service which gets the current instance of our own "caller context". There are different implementations of this service, but the "web" implementation stores the current context in `HttpContext.Items`. However, if a background task were started with an HttpContext, then our context might get disposed too early, and the background task would have its context pulled out from under it. So for background tasks, we want to default the service to associate our context with the Thread, rather than the HttpContext. – RobSiklos Dec 15 '20 at 01:10
  • This means your service will have trouble using asynchronous operations as tasks aren't bound to any specific thread. Especially in web applications, continuations can run on any available thread – Panagiotis Kanavos Dec 15 '20 at 06:50
  • @PanagiotisKanavos yes that's correct. We need to re-establish our context in any continuations (unless an HttpContext is present). For the future, we may take advantage of `AsyncLocal<>` to help with this. – RobSiklos Dec 15 '20 at 13:33
  • Have you considered inverting that dependency? Instead. of having the service pull data from the HttpContext, provide any necessary data to it from the start, or when a new request is submitted. Right now you're trying to handle a transient hard-coded dependency. Why not eliminate it? – Panagiotis Kanavos Dec 15 '20 at 13:36
  • @PanagiotisKanavos We need some way to ask the service for the "current" context (we use our service in a way kind of like IHttpContextAccessor), so I'm not sure how that would work, unless we start using `AsyncLocal<>`, which would be a good change, but unfortunately this class didn't exist during the initial development of our service. – RobSiklos Dec 15 '20 at 14:15

0 Answers0