Start a task with clean AsyncLocal state
Asked Answered
B

2

11

In ASP.NET Core, I am using IHttpContextAccessor to access the current HttpContext. HttpContextAccessor uses AsyncLocal<T>.

In one situation, I am trying to start a Task from within a request, that should continue running after the request has ended (long running, background task), and I am trying to detach it from the current execution context so that IHttpContextAccessor returns null (otherwise I am accessing an invalid HttpContext).

I tried Task.Run, Thread.Start, but every time, the context seems to carry over and IHttpContextAccessor returns the old context whatever I try (sometimes even contexts from other requests πŸ€”).

How can I start a task that will have a clean AsyncLocal state so that IHttpContextAccessor returns null?

Bidet answered 26/6, 2017 at 10:43 Comment(7)
You should avoid running/spawning your own threads/background tasks in ASP.NET Core anyways, it will mess up in the way how ASP.NET Core handles threads for incoming requests. You should do it properly with a kind of local or remote message bus and have real background processes (i.e. console application that do not host asp.net core) process it – Wrung
At the very least I should be able to create my own thread without interfering with Kestrel's request handling. Even that doesn't work, and captures some random HttpContext. – Bidet
Task.Run(...), etc. has a parameter where you can set the scheduler. You could set the default scheduler in here (ASP.NET Core runs its own scheduler), i.e. Task.Run(() => DoSomething(), TaskScheduler.Default). But again, I do not recommend you doing that and you should consider a real background process like described above, because threads taken away for background tasks are threads which can't be used to accept request. ASP.NET Core should just use await/async for true async operation, not CPU intensive stuff or background tasks – Wrung
Also useful resource hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx haacked.com/archive/2011/10/16/…, while it's mostly about ASP.NET 4, it's concepts still apply – Wrung
That's not true, ASP.NET Core uses threads from the thread pool to process requests. New threads (var t = new Thread(...)) are not taken from the thread pool, so they will not interfere with request handling. – Bidet
msdn.microsoft.com/en-us/magazine/dn802603.aspx As a general rule, don’t queue work to the thread pool on ASP.NET. And it is still valid for ASP.NET Core. Also Task.Run will take away a thread from the requestpool, since it's run with the current context's scheduler, which is ASP.NET Core's scheduler. Most important argument though is, you have no guarantees it will ever complete (i.e. IIS could shut down your ASP.NET Core process and with it your background tasks at any time for any reason) – Wrung
I'm not using IIS. Unlike IIS, Kestrel doesn't recycle its AppDomains (in fact, it doesn't have AppDomains at all). The background task being interrupted is not a concern in my use case anyway. And again, Thread.StartNew spawns a completely new thread which does not come from the thread pool, so your previous comment is beside the point. – Bidet
C
2

You could await the long running task and set HttpContext to null inside await. It would be safe for all code outside the task.

[Fact]
public async Task AsyncLocalTest()
{
    _accessor = new HttpContextAccessor();

    _accessor.HttpContext = new DefaultHttpContext();

    await LongRunningTaskAsync();

    Assert.True(_accessor.HttpContext != null);
}

private async Task LongRunningTaskAsync()
{
    _accessor.HttpContext = null;
}

You could run a separate thread, it doesn't matter.

Task.Run(async () => await LongRunningTaskAsync());

Just make the task awaitable to cleanup HttpContext for the task's async context only.

Cleanse answered 26/6, 2017 at 11:11 Comment(5)
Doesn't seem to work for me. Accessing httpContextAccessor.HttpContext from inside the long running task still returns a non-null HttpContext. Also, I can't await the long running task from the request as I want the request to complete now, irrespective of how long the long running tasks runs for. – Bidet
@Flavien, you could use Task.Run to start the task in separate thread, it doesn't matter. The point is to place await between the caller and the task. For example: Task.Run(async () => await LongRunningTaskAsync()); – Cleanse
I tried that, but even with the await, it doesn't seem to make a difference. The HttpContext is still present. – Bidet
Ok, this works as long as I reuse the same instance of HttpContextAccessor. – Bidet
@Flavien, yes, it sould be registered as singleton. – Cleanse
K
11

Late answer but might be useful for someone else. You can do this by suppressing the ExecutionContext flow as you submit your task:

ExecutionContext.SuppressFlow();

var task = Task.Run(async () => {
    await LongRunningMethodAsync();
});

ExecutionContext.RestoreFlow();

or better:

using (ExecutionContext.SuppressFlow()) {
    var task = Task.Run(async () => {
        await LongRunningMethodAsync();
    });
}

This will prevent the ExecutionContext being captured in the submitted task. Therefore it will have a 'clean' AsyncLocal state.

As above though I wouldn't do this in ASP.net if it's CPU intensive background work.

Keavy answered 7/9, 2018 at 11:49 Comment(2)
A notable downside of this solution is that it suppresses the flow of anything else into the child task as well: SuppressFlow() is a fairly brute-force technique: You can flow everything or nothing. Lots of other data hangs off ExecutionContext besides just the HttpContext, so if you use this solution, you need to be prepared to have none of the other context data in the child task either, which includes things like the current culture and the logical call stack. – Chasten
Why the need for Task.Run(...)? Shouldn't SuppressFlow() by itself do the trick? – Duckling
C
2

You could await the long running task and set HttpContext to null inside await. It would be safe for all code outside the task.

[Fact]
public async Task AsyncLocalTest()
{
    _accessor = new HttpContextAccessor();

    _accessor.HttpContext = new DefaultHttpContext();

    await LongRunningTaskAsync();

    Assert.True(_accessor.HttpContext != null);
}

private async Task LongRunningTaskAsync()
{
    _accessor.HttpContext = null;
}

You could run a separate thread, it doesn't matter.

Task.Run(async () => await LongRunningTaskAsync());

Just make the task awaitable to cleanup HttpContext for the task's async context only.

Cleanse answered 26/6, 2017 at 11:11 Comment(5)
Doesn't seem to work for me. Accessing httpContextAccessor.HttpContext from inside the long running task still returns a non-null HttpContext. Also, I can't await the long running task from the request as I want the request to complete now, irrespective of how long the long running tasks runs for. – Bidet
@Flavien, you could use Task.Run to start the task in separate thread, it doesn't matter. The point is to place await between the caller and the task. For example: Task.Run(async () => await LongRunningTaskAsync()); – Cleanse
I tried that, but even with the await, it doesn't seem to make a difference. The HttpContext is still present. – Bidet
Ok, this works as long as I reuse the same instance of HttpContextAccessor. – Bidet
@Flavien, yes, it sould be registered as singleton. – Cleanse

© 2022 - 2024 β€” McMap. All rights reserved.