2

I have an ASP.NET MVC web application.

At a certain point, the UI user makes a POST to the server. The server has to do some heavy operations in a different thread, and return a response to the user as soon as possible.

The response sent back to the UI does NOT depend on the result of the heavy operations, so the UI does not need to be blocked till the heavy operations are done. This POST method should behave like a trigger for some big computational stuff. The user should be notified immediately that the server has started working on the heavy operations.

A skeleton for what should happen is:

[HttpPost]
public ActionResult DoSomething(PostViewModel model)
{
    //////////////////////////
    /*
        * The code in this section should run asynchronously (in another thread I guess).
        * That means the UI should not wait for any of these operations to end.
        * 
        * */
    ComputeHeavyOperations();
    //////////////////////////


    //the response should be returned immediatelly 
    return Json("Heavy operations have been triggered.");
}

private void ComputeHeavyOperations()
{
    //execute some heavy operations; like encoding a video 
}

How can I implement something like this?

halfer
  • 19,824
  • 17
  • 99
  • 186
mihai94stk
  • 69
  • 1
  • 4
  • Usually what happens is that the web page puts the data somewhere and a separate process on the server manipulates the data and sets a notification that the web page periodically checks. Trying to do this in the IIS thread pool is sure to weigh down the web side, even if you offloaded it to a separate thread. – Ron Beyer Feb 23 '18 at 05:20
  • The heavy computational stuff don't actually run on the same machine. The encoding is done by an external service. All that my application is doing in the ComputeHeavyOperations() method is to wait for the service to complete its actions. – mihai94stk Feb 23 '18 at 05:23
  • Are you aware of the async and await keywords? – see sharper Feb 23 '18 at 05:24
  • @seesharper yes I am aware. But I just can't manage to get it right. Can you provide some code samples? – mihai94stk Feb 23 '18 at 05:25
  • Or run `ComputeHeavyOperations()` method is separate thread as `new System.Threading.Thread(() => ComputeHeavyOperations()).Start();` – mmushtaq Feb 23 '18 at 05:26
  • @mmushtaq I already did that. But the response is not sent back to the server until ComputeHeavyOperations() is also run. It's like the UI thread waits for every other thread to complete. – mihai94stk Feb 23 '18 at 05:27
  • Strange. I am sending emails to my users on separate thread and its working fine and no blocking UI thread. – mmushtaq Feb 23 '18 at 05:30
  • 2
    Consider a framework such as HangFire (refer [How to run Background Tasks in ASP.NET](https://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx) –  Feb 23 '18 at 05:30
  • Take a look at https://stackoverflow.com/questions/18502745/fire-and-forget-async-method-in-asp-net-mvc – see sharper Feb 23 '18 at 05:33
  • @mmushtaq spinning up long-running threads in ASP.NET is not recommended since ASP.NET has no idea what you are doing and your AppPool is at danger of recycling –  Feb 23 '18 at 05:52
  • @MickyD then what should we do? if I want to run a process in background? For me there were two options 1. use async programming 2. Start new thread. My application is in .net4 and async is available in .net4.5, so i went for 2nd option. What are the other work around for that? – mmushtaq Feb 23 '18 at 06:11
  • You need to use a framework that is aware of the IIS ecosystem –  Feb 23 '18 at 06:34

3 Answers3

1

You can use Queued background tasks and implement BackgroundService. This link is useful.

public class BackgroundTaskQueue : IBackgroundTaskQueue
    {
        private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
            new ConcurrentQueue<Func<CancellationToken, Task>>();
        private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);

        public void QueueBackgroundWorkItem(
            Func<CancellationToken, Task> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }

            _workItems.Enqueue(workItem);
            _signal.Release();
        }

        public async Task<Func<CancellationToken, Task>> DequeueAsync(
            CancellationToken cancellationToken)
        {
            await _signal.WaitAsync(cancellationToken);
            _workItems.TryDequeue(out var workItem);

            return workItem;
        }
    }

In QueueHostedService, background tasks in the queue are dequeued and executed as a BackgroundService, which is a base class for implementing a long running IHostedService:

public class QueuedHostedService : BackgroundService
    {
        private readonly ILogger _logger;

        public QueuedHostedService(IBackgroundTaskQueue taskQueue,
            ILoggerFactory loggerFactory)
        {
            TaskQueue = taskQueue;
            _logger = loggerFactory.CreateLogger<QueuedHostedService>();
        }

        public IBackgroundTaskQueue TaskQueue { get; }

        protected override async Task ExecuteAsync(
            CancellationToken cancellationToken)
        {
            _logger.LogInformation("Queued Hosted Service is starting.");

            while (!cancellationToken.IsCancellationRequested)
            {
                var workItem = await TaskQueue.DequeueAsync(cancellationToken);

                try
                {
                    await workItem(cancellationToken);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        $"Error occurred executing {nameof(workItem)}.");
                }
            }

            _logger.LogInformation("Queued Hosted Service is stopping.");
        }
    }

The services are registered in Startup.ConfigureServices. The IHostedService implementation is registered with the AddHostedService extension method:

services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();

In the controller : The IBackgroundTaskQueue is injected into the constructor and assigned to Queue. An IServiceScopeFactory is injected and assigned to _serviceScopeFactory. The factory is used to create instances of IServiceScope, which is used to create services within a scope. A scope is created in order to use the app's AppDbContext (a scoped service) to write database records in the IBackgroundTaskQueue (a singleton service).

public class SomeController : Controller
{
    private readonly AppDbContext _db;
    private readonly ILogger _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public SomeController(AppDbContext db, IBackgroundTaskQueue queue, 
        ILogger<SomeController> logger, IServiceScopeFactory serviceScopeFactory)
    {
            _db = db;
            _logger = logger;
            Queue = queue;
            _serviceScopeFactory = serviceScopeFactory;
    }

    public IBackgroundTaskQueue Queue { get; }

    [HttpPost]
    public ActionResult DoSomething(PostViewModel model)
    {
        //////////////////////////
        /*
            * The code in this section should run asynchronously (in another thread I guess).
            * That means the UI should not wait for any of these operations to end.
            * 
            * */
        ComputeHeavyOperations();
        //////////////////////////


        //the response should be returned immediatelly 
        return Json("Heavy operations have been triggered.");
    }

    private void ComputeHeavyOperations()
    {
        Queue.QueueBackgroundWorkItem(async token =>
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var scopedServices = scope.ServiceProvider;
                var db = scopedServices.GetRequiredService<AppDbContext>();

                    try
                    {
                        //use db to crud operation on database
                        db.doSomeThingOnDatabase();
                        await db.SaveChangesAsync();
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, 
                            "An error occurred writing to the " +
                            $"database. Error: {ex.Message}");
                    }

                    await Task.Delay(TimeSpan.FromSeconds(5), token);        

      }
        _logger.LogInformation(
            "some background task have done on database successfully!");
    });
} }
0

you can use async and await

[HttpPost]
public async Task<ActionResult> DoSomething(PostViewModel model)
{                    
    Task.Run(async () => await ComputeHeavyOperations());
    //the response should be returned immediatelly 
    return Json("Heavy operations have been triggered.");
}

private  async void ComputeHeavyOperations()
{
    //execute some heavy operations; like encoding a video 
    // you can use Task here
}
lazydeveloper
  • 891
  • 10
  • 20
0

Task.Factory?.StartNew(() => ComputeHeavyOperations(), TaskCreationOptions.LongRunning);

Anand
  • 1
  • 2
  • Spinning up long-running Tasks/threads in ASP.NET is not recommended since ASP.NET has no idea what you are doing and your AppPool is at danger of recycling –  Feb 23 '18 at 23:23