18

I have an ASP.NET Core website, using EFCore. I would like to do some work like logging to the database, but after having sent the response to the user in order to answer faster.

I could do it in a different thread, but due to async access of the DbContext I am not sure it is safe. Is there any recommended way to do that?

public async Task<IActionResult> Request([FromForm]RequestViewModel model, string returnUrl = null)
{
    try 
    {
      var newModel = new ResponseViewModel(model);
      // Some work 
      return View("RequestView",newModel)
    }
    finally
    {
        // Some analysis on the request
        // I would like to defer this part
        await Log(model);
    }
}

One of the reason is that I would like to call a web-service (geocoding), which is not needed to answer, but good to work on the log (I need the city/country of coordinates).

Jean
  • 4,911
  • 3
  • 29
  • 50
  • 1. What are you doing that your logging is so much slower than your request to slow down the whole thing? Sounds like microoptimization where it may not be necessary at all 2. Starting threads in ASP.NET (Core or legacy) is very bad and messes with the way ASP.NET manages threads from threadpool, usually leading to worse instead of better performance (memory allocation for the thread, context switches). 3. If you really insist and have a solid data to back up that this is a bottle neck, queue the messages (memory or distributed) and process them in the background) – Tseng Jan 16 '17 at 00:49
  • It is not really "heavy", but needs some read/write to the database (check if the request has already been done for example), I just would like to be able to answer before doing that as it does not give anything to the user, who would prefer to get his response. – Jean Jan 16 '17 at 10:39

5 Answers5

25

I see this has never been answered, but actually have a solution. The simple solution:

public async Task<IActionResult> Request([FromForm]RequestViewModel model, string returnUrl = null)
{
    try 
    {
      var newModel = new ResponseViewModel(model);
      // Some work 
      return View("RequestView",newModel)
    }
    finally
    {
        Response.OnCompleted(async () =>
        {
            // Do some work here
            await Log(model);
        });
    }
}

The secure solution, as OnCompleted used to be called before the response being sent, so delaying the response:

public static void OnCompleted2(this HttpResponse resp, Func<Task> callback)
{
    resp.OnCompleted(() =>
    {
        Task.Run(() => { try { callback.Invoke(); } catch {} });
        return Task.CompletedTask;
    });
}

and call Response.OnCompleted2(async () => { /* some async work */ })

Jean
  • 4,911
  • 3
  • 29
  • 50
  • 13
    Just pointing out that the bug that OnCompleted was called before sending the response has been fixed since ASP.NET Core 2.1.0-preview2, so the first solution is OK if you are using a later version: [GitHub issue](https://github.com/aspnet/KestrelHttpServer/issues/2035) – scharnyw Jul 31 '19 at 08:15
  • If you need to set this in a different class than the controller, you can inject IHttpContextAccessor and set accessor.HttpContext?.Response.OnCompleted(() => your-callback) – jornhd Jan 20 '23 at 15:38
5

Building on Jeans answer and a question and answer on the try - return - finally pattern, the try and finally blocks can be removed (if you don't really want to catch an exception).

This leads to the following code:

public async Task<IActionResult> Request([FromForm] RequestViewModel model, string returnUrl = null)
{
    var newModel = new ResponseViewModel(model);

    // Some work 

    Response.OnCompleted(async () =>
    {
        // Do some work here
        await Log(model);
    });

    return View("RequestView", newModel);
}
Alexander Trauzzi
  • 7,277
  • 13
  • 68
  • 112
Johannes Buchholz
  • 1,857
  • 19
  • 34
1

There's no out of the box way to do what you want.

But, here's a possible approach:

  1. Have a queue and a worker (thread or process)
  2. Just before the request is sent back to the client, add a message in that queue
  3. The worker will pick up that message at some point in the future, and process it.

Since the worked runs somewhere else and not on the request thread, the server can complete the request thread and the worker can do what's left.

Victor Hurdugaci
  • 28,177
  • 5
  • 87
  • 103
  • That is one option I was thinking about, even if not optimal, it is probably the most efficient. My alternate option is to log the "raw" request and post-process the raw requests in a windows service, independent from ASP... As EF is not very friendly with threads I am not sure about the best solution (the second one will for sure not be an issue, but requires more work to set up and updates) – Jean Jan 16 '17 at 10:44
0

Try using Hangfire. Hangfire is an easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required. Backed by persistent storage. Open and free for commercial use.

You could do something like

var jobId = BackgroundJob.Enqueue(() => Log(model));

And here is my blog post on using HangFire in ASP.NET Core

Anuraj
  • 18,859
  • 7
  • 53
  • 79
-2

Create a new class that inherits from ActionFilterAttribute, overwrite the OnResultExecuted method to perform the logging and then apply your attribute class to the controller actions you want to do logging.

NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • 2
    That's not gonna bring the intended goal, as he wants to do logging **after* the request has **finished**. ActionFilter is still executed while the request still runs – Tseng Jan 16 '17 at 00:50