ASP.NET MVC2 Call AsyncController method from jQuery?

2.5k views Asked by At

I'm trying to learn how to use AsyncController in MVC2, but there is very little documentation/tutorials out there. I'm looking to take one normal controller method that has a very slow export to a 3rd party service and convert that to an async method.

The original controller method:

public JsonResult SaveSalesInvoice(SalesInvoice invoice)
{
    SaveInvoiceToDatabase(invoice); // this is very quick 
    ExportTo3rdParty(invoice); // this is very slow and should be async
}

So I created a new controller that inherits from AsyncController:

public class BackgroundController : AsyncController
{
    public void ExportAysnc(int id)
    {
        SalesInvoice invoice = _salesService.GetById(id);
        ExportTo3rdParty(invoice);
    }

    public void ExportCompleted(int id)
    {
         // I dont care about the return value right now, 
         // because the ExportTo3rdParty() method
         // logs the result to a table
    }

    public void Hello(int id)
    {            
    }
}

And then call the Export method from jQuery:

function Export() {
    $.post("Background/Export", { id: $("#Id").val() }, function (data) {
    // nothing to do yet
    });
}

BUT the result is a 404 not found error (Background/Export is not found). If I try to call Background/Hello or Background/ExportAysnc they are found.

What am I doing wrong?

1

There are 1 answers

6
Darin Dimitrov On BEST ANSWER

There are indeed two use cases

  1. You care about the result of the lengthy operation
  2. You don't care about the result (fire and forget)

Let's start with the first case:

public class BackgroundController : AsyncController
{
    public void ExportAysnc(int id)
    {
        AsyncManager.OutstandingOperations.Increment();

        Task.Factory.StartNew(() => DoLengthyOperation(id));

        // Remark: if you don't use .NET 4.0 and the TPL 
        // you could manually start a new thread to do the job
    }

    public ActionResult ExportCompleted(SomeResult result)
    {
        return Json(result, JsonRequestBehavior.AllowGet);
    }

    private void DoLengthyOperation(int id)
    {
        // TODO: Make sure you handle exceptions here
        // and ensure that you always call the AsyncManager.OutstandingOperations.Decrement()
        // method at the end
        SalesInvoice invoice = _salesService.GetById(id);
        AsyncManager.Parameters["result"] = ExportTo3rdParty(invoice);
        AsyncManager.OutstandingOperations.Decrement();
    }
}

Now you could invoke it like this:

$.getJSON(
    '<%= Url.Action("Export", "Background") %>', 
    { id: $("#Id").val() }, 
    function (data) {
        // do something with the results
    }
);

Now because you have mentioned a web service call, this means when you generated the client proxy of your web service you had the chance to emit async methods (XXXCompleted and XXXAsync):

public class BackgroundController : AsyncController
{
    public void ExportAysnc(int id)
    {
        AsyncManager.OutstandingOperations.Increment();
        // that's the web service client proxy that should
        // contain the async versions of the methods
        var someService = new SomeService();
        someService.ExportTo3rdPartyCompleted += (sender, e) =>
        {
            // TODO: Make sure you handle exceptions here
            // and ensure that you always call the AsyncManager.OutstandingOperations.Decrement()
            // method at the end

            AsyncManager.Parameters["result"] = e.Value;
            AsyncManager.OutstandingOperations.Decrement();
        };
        var invoice = _salesService.GetById(id);
        someService.ExportTo3rdPartyAsync(invoice);
    }

    public ActionResult ExportCompleted(SomeResult result)
    {
        return Json(result, JsonRequestBehavior.AllowGet);
    }
}

This is the best possible usage of an async controller as it relies on I/O Completion Ports and doesn't monopolize any threads on the server during the execution of the lengthy operation.


The second case is easier (don't really need an async controller):

public class BackgroundController : Controller
{
    public ActionResult Export(int id)
    {
        // Fire and forget some lengthy operation
        Task.Factory.StartNew(() => DoLengthyOperation(id));
        // return immediately
        return Json(new { success = true }, JsonRequestBehavior.AllowGet);
    }
}

Here's a nice article on MSDN on Async controllers.