0

I am using MVC 5 on .Net and I have a user flow that looks like this:

  1. User enters address into an form.
  2. Form gets posted to controller using AJAX.
  3. Controller records address into the database.
  4. Controller makes a WebClient request to Bing Maps to geocode the address into latitude and longitude.
  5. Controller records latitude and longitude to the database.
  6. Controller returns an AJAX result that is rendered client-side into the updated view with the address and the latitude/longitude.

I know that the call to Bing Maps should happen in an async context so that my site's speed is uncoupled from that of Bing Maps.

Instead I think my flow should work like this:

  1. User enters address into an form.
  2. Form gets posted to controller using AJAX.
  3. Controller records address into the database.
  4. Controller launches an async task to do the geocoding and update the database
  5. Controller returns an AJAX result to the client that shows the updated address and tells it to poll client-side for the completion of the geocode result.

I am stuck on step #4. Here is what I have:

public ActionResult GetLocation(int id)
{
    Listing li = db.Listings.Find(id);

    Task.Run(() => {
        // update geocode if necessary
        if (li.BizAddress.GeoStatus != BusinessAddress.GeocodeStatus.UpToDate &&
            DateTime.Now - li.BizAddress.LastGeoAttempt > TimeSpan.FromHours(1))
        {
            Geocoder geo = new Geocoder();
            GeocodeResult gr = geo.Geocode(li.BizAddress).Result;
            if (gr.BadResult != true)
            {
                li.BizAddress.Latitude = gr.Location.Latitude;
                li.BizAddress.Longitude = gr.Location.Longitude;
                li.BizAddress.GeoStatus = BusinessAddress.GeocodeStatus.UpToDate;

            }
            else
            {
                // failed
                li.BizAddress.Latitude = 0;
                li.BizAddress.Longitude = 0;
                li.BizAddress.GeoStatus = BusinessAddress.GeocodeStatus.BadResult;
            }

            li.BizAddress.LastGeoAttempt = DateTime.Now;
            db.SaveChanges();
        }

    });


    return PartialView("~/Views/Listing/ListingPartials/_Location.cshtml", li);
}

However I get an exception that the db context has been disposed of when I get to db.SaveChanges().

I want my Task to run async inside of a closure so that db is still a valid undisposed variable.

Is this possible? I'm new to async programming and I don't know all of the idioms yet.

Additional info: I am rendering my panel in my View like this:

@{Html.Action("GetLocation", new { id = Model.ID });}

I want to keep this behavior since loading it with AJAX may hurt SEO.

John Shedletsky
  • 7,110
  • 12
  • 38
  • 63
  • Unsure if I completely understood, but IINM in #5 in what you want to do (poll), that would still be _client-side_ so that "breaks" your last statement (loading with AJAX will hurt SEO) (?) - your "other content" would be "done" (rendered) with the exception of the Bing call (anyway). I'm no guru, so the for the exception part, you are firing a separate task (it's own context) from the ASP.net request context - so it's not "linear" - the ASP.Net request context is "done" before your `Task` (which is why as in the answer below use `async/await` ) – EdSF Jul 17 '15 at 01:07

1 Answers1

0

Firing off a Task in this way is not good practice. Instead, mark your ActionResult signature as async and then await the results that you need from inside the method body. This way you won't be tying up server resources waiting for the response from Bing. That also frees you up from having to worry about instructing the client side to poll for a future result as I would consider this an anti-pattern in async programming. It's much easier to just deal with it all in one call and send the result when it's available. Since you're using EF6 you can also leverage async methods to improve server performance.

public async Task<ActionResult> GetLocation(int id)
{
    var listing = await db.Listings.FindAsync(id);
    ...
    var gr = await geo.Geocode(li.BizAddress);

At the very least, if you're sure you want to go down this path its important to clean up your Task code. It's very bad practice to use .Result with Tasks inside of a web server as it locks up server threads. Change the definition of your Task so that it is async:

Task.Run(async () => {
        // update geocode if necessary
        if (li.BizAddress.GeoStatus != BusinessAddress.GeocodeStatus.UpToDate &&
            DateTime.Now - li.BizAddress.LastGeoAttempt > TimeSpan.FromHours(1))
        {
            Geocoder geo = new Geocoder();
            GeocodeResult gr = await geo.Geocode(li.BizAddress);
Jesse Carter
  • 20,062
  • 7
  • 64
  • 101
  • The problem with this, as I understand it, is that now GetLocation blocks on the geocoding step before returning the PartialView. I want to return immediately. Then have an async process write its result to the database at some later point. – John Shedletsky Jul 16 '15 at 22:43
  • @JohnShedletsky Hmmm, yeah I didn't realize that you were calling this ActionResult method from an `@ActionResult` in Razor – Jesse Carter Jul 16 '15 at 22:45
  • There must be a pattern for dealing with this, I just don't know what to google for. – John Shedletsky Jul 16 '15 at 22:45
  • Where are you getting your instance of `db` from? Its not clear in your code. There's no need to pass it inside the closure of your Task, just create a new instance inside the Task and then you won't need to worry about it being disposed. – Jesse Carter Jul 16 '15 at 22:46
  • It is a private ivar of my Controller. Just following what I saw in the starter code. I will try what you suggest, which is pretty clever. How will SaveChanges know what the changes are in this circumstance? It would have to be doing crazy voodoo to still work? – John Shedletsky Jul 16 '15 at 22:48
  • Is this Entity Framework? It's generally encouraged to make an instance of db per request instead of trying to share the instance especially when dealing with async stuff to avoid the exact sorts of issues you're running in to. – Jesse Carter Jul 16 '15 at 22:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/83490/discussion-between-jesse-carter-and-john-shedletsky). – Jesse Carter Jul 16 '15 at 22:49