Making fire-and-forget calls in a thread-safe manner in C#

444 Views Asked by At

I have the following situation: I have an ASP.NET Core controller (.NET 5.0), which has an Import endpoint. Upon calling this endpoint it gathers some configuration and calls

MyService.Import(...)

MyService.Import performs some time-consuming operations - it retrieves data from external endpoints (with paging and retries), and then does some heavy lifting on the database.

The controller method can't afford to wait for the service method to finish. It would simply take too long and the callers of my endpoint would get a timeout error. And it's not supposed to anyway, MyService has its own ways of reporting about the status of the import - via logs. This is supposed to be a fire-and-forget call.

I've tried to achieve this fire-and-forget behavior in the following ways:

  1. Make the service method async and just don't await it. The warning I get from this says "Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the await operator to the result of the call." So I would expect that to be the behavior - the calling (controller) method continues and reaches return Ok(), while the service method does its thing. This isn't what happens. The controller method just stands still waiting for the service method it ostensibly isn't awaiting.

  2. Await the method, but put ConfigureAwait(false) at the end. Same result as above.

  3. Calling the service method on a different thread via Task.Run(() => MyService.Import(...). This approach actually allows the controller method to finish without waiting for the service method. But this causes errors further down the pipe. Somewhere in the bowels of the code, through many layers of DI-initialized providers and resolvers, something was broken by switching the thread, and I get the following error:

    System.ObjectDisposedException: 'Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
    Object name: MyDbContext

Re-working the infrastructure so that it can handle switching threads is probably a project for a week. Is there a way have the controller method return without threading? Or some way to "pass the entire context" to the new thread?

2

There are 2 best solutions below

0
David Browne - Microsoft On

. But this causes errors further down the pipe. Somewhere in the bowels of the code,

If you kick of an async task it can't access any resources tied to the HTTP request lifecycle, like your DbContext or other scoped services. It needs to create its own scope, or simply create a new instance of your DbContext for the long-running task.

2
Shaggydog On

So I dug through the code and my colleague actually solved it the following way:

Response.OnCompleted(async () => MyService.Import(...
return Ok();

This obviously works only for this specific case. I'm not sure how "correct" this is, or if there is some resource on the server that's stuck waiting for MyService.Import, but it does achieve the desired behavior wherein the controller method returns a response without waiting for the service to finish.