Search code examples
c#.net.net-corefire-and-forget

Fire-and-forget in .Net Core Web API - approaches and observations


We have a .Net Core 3.1 Web API that, towards the end of the request processing flow, deep in the services layer, needs to complete a fire-and-forget task.

Disclaimer: I am aware that the approach taken is generally considered less than ideal, but it has been accepted by the team, along with its risks.

Below is the current solution in a nutshell:

public class MyService
{
    public readonly IServiceA _serviceA;
    public readonly IServiceB _serviceB;
    
    public MyService(IServiceA serviceA, IServiceB serviceB)
    {
        _serviceA = serviceA;
        _serviceB = serviceB;
    }

    public void FireTheTask(MyData data)
    {
         _ = Task.Run(() => Submit(data));
    }

    public async Task Submit(MyData data)
    {
        try {
            var result = await _serviceA.CallSomeService(data); // uses HttpClientFactory

            if(!result.Success)
            {
                _serviceB.SaveData(data); // for offline processing
            }
        }
        catch (Exception ex)
        {
            // Log error to file here.
        }
    }
}

Both serviceA and serviceB are registered as transient and injected through DI.

There's technically a risk that the main request will complete sooner than Submit(), and serviceA and serviceB will no longer be available.

However, my consistent observation is that even if Submit() starts with, e.g., await Task.Delay(30000), it still successfully completes, 30 seconds after the main request has completed, and a response is returned from the API.

  1. Can anyone please comment on / explain this observation?

  2. If there's a risk of the above code not working reliably, would the solution below be correct (assuming no objects from the main request scope need to be referenced by the services)?

public class MyService
{
    public readonly IServiceScopeFactory _scopeFactory;
    
    public MyService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public void FireTheTask(MyData data)
    {
         _ = Task.Run(() => Submit(data));
    }

    public async Task Submit(MyData data)
    {
        try {
            using var scope = _scopeFactory.CreateScope();
            
            var serviceA = scope.ServiceProvider.GetRequiredService<IServiceA>();
            var serviceB = scope.ServiceProvider.GetRequiredService<IServiceB>();
            
            var result = await serviceA.CallSomeService(data); // uses HttpClientFactory

            if(!result.Success)
            {
                serviceB.SaveData(data); // for offline processing
            }
        }
        catch (Exception ex)
        {
            // Log error to file here.
        }
    }
}
  1. Would any transient services that serviceA and serviceB depend on (i.e., have injected via their constructors) be taken care of by this solution?

  2. Could this solution be further improved or simplified, and if so, how?

Thank you.


Solution

  • As you mentioned in comments, the services referenced MyService are not disposable.

    That is, when the DI scope is closed, nothing happens with those instances, and the code which has references to those services can access them.

    However, you’re worrying right, that there might be issues with the services when they are transient and they are used like in your fire-and-forget example. You solution is right - the Submit method lifetime doesn’t depend on the Request lifetime, and vice versa, therefore yes, you create a separate scope for this.

    I understand that both solutions are technically acceptable, correct

    I see here only one solution - the one with scope. If you mean the original code, then it is a bad idea, even if it works now. It will break once you add any dependency to your service, which can be disposed(or its dependency, or … and so on).

    So, answering the questions:

    1. See this answer
    2. Yes
    3. Yes
    4. I don’t see improvements here apart from syntax sugar, which everyone puts depending on the taste.

    P.S. you can see this answer it may help to understand di lifecycle