Click here to Skip to main content
Click here to Skip to main content

Scheduling Tasks with Revalee and MVC (Part 2)

, 23 May 2014
Rate this:
Please Sign up or sign in to vote.
An example of asynchronously sending email notifications after a long delay from an MVC controller.

Click here to read the previous part of this article: Scheduling tasks with Revalee and MVC.

Introduction

You're proud of yourself, admit it. Your new MVC application is scheduling web tasks like a charm via Revalee, despite Ted's best efforts to interrupt things. (Ted is the guy at your company who's always messing things up or just not doing them. Yeah, he's THAT guy.) Anyway, life is good. Ahhh...

But wait, why is Ted walking over to your desk? He smiles that smile. Slowly, the hair on the back of your neck stands up. "Uh... what's up, Ted?" (Something's off.) "Users are starting to complain about the application's performance. Your app isn't very good." And with that Ted walks away, smiling even more, his work done for the day confident that he's ruined yours completely.

Ugh. Now what? What did you miss?

And then it hits you like a Mack Truck: Your entire web application uses a synchronous architecture.

Background

Wait, what? Are you stuck in the year 2000 or something? (Ring, ring. "Hello?" "Yeah hi, it's the year 2000. I want my web app back.") Sure you tested the thing locally with 10 users, but what about the 10,000 users hitting your app right now? Most of the time, the threads in your application's AppPool are just waiting for some I/O heavy processes to finish. When your application is synchronously architected, it simply won't scale as well to handle boatloads of users, especially when your application includes a lot of I/O intensive processes.

So you take the time to re-architect your application to be fully asynchronous. Well done. But what about those Revalee callbacks that your app schedules? Worry not. Revalee is ready to work with you. Why have a Revalee callback block your worker thread while waiting for its web service call to complete?

How do I do that, you ask? Well, I'm glad you did...

Disclaimer: Revalee is a free, open source project written by the development team that I am a part of. It is available on GitHub and is covered by the MIT License. If you are interested, download it and check it out.

A Quick Refresher

Last time, you scheduled a synchronous callback using Revalee in your MVC application. To review, here's a more fleshed out code snippet from your Controller of that effort:

using Revalee.Client;

// ...

[HttpPost]
public ActionResult SignUp(string email, string name)
{
    // TODO Validate the parameters

    User user = DbContext.CreateNewUser(email, name);

    string revaleeServiceHost = "172.31.46.200";

    Uri welcomeMessageCallbackUri = new Uri(
        string.Format("http://mywebapp.com/ScheduledCallback/SendWelcomeMessage/{0}", user.userId));

    Guid welcomeMessageCallbackId = RevaleeRegistrar.ScheduleCallback(
        revaleeServiceHost, DateTimeOffset.Now, welcomeMessageCallbackUri);

    Uri expirationMessageCallbackUri = new Uri(
        string.Format("http://mywebapp.com/ScheduledCallback/SendExpirationMessage/{0}", user.userId));

    Guid expirationMessageCallbackId = RevaleeRegistrar.ScheduleCallback(
        revaleeServiceHost, TimeSpan.FromDays(27.0), expirationMessageCallbackUri);

    return View(user);
}

[AllowAnonymous]
[HttpPost]
public ActionResult SendWelcomeMessage(int userId)
{
    if(!RevaleeRegistrar.ValidateCallback(this.Request))
    {
        return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
    }

    // TODO Validate the parameter,
    //      lookup the user's information, and
    //      compose & send the welcome message

    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

[AllowAnonymous]
[HttpPost]
public ActionResult SendExpirationMessage(int userId)
{
    if(!RevaleeRegistrar.ValidateCallback(this.Request))
    {
        return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
    }

    // TODO Validate the parameter,
    //      lookup the user's information, and
    //      compose & send the welcome message

    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

// ...

That works great. However, when you need scalability, asynchronous processing is the way to go. So after Ted burst your bubble (<shake-fist-at-the-sky>, as appropriate), you went ahead and re-architected most your application to do just that. Here's how you would approach the Revalee-related portions of your web application.

Revalee... Asynchronously

Using the code snippet above as your stake in the ground, here is an asynchronous version of that same code snippet:

using Revalee.Client.Mvc;

// ...

[HttpPost]
[RevaleeClientSettings(ServiceBaseUri = "172.31.46.200")]
public async Task<ActionResult> SignUp(string email, string name)
{
    // TODO Validate the parameters

    User user = await DbContext.CreateNewUserAsync(email, name);

    Guid welcomeMessageCallbackId = await this.CallbackToActionAtAsync(
        "SendWelcomeMessage", new { @userId = user.UserId }, DateTimeOffset.Now);

    Guid expirationMessageCallbackId = await this.CallbackToActionAfterAsync(
        "SendExpirationMessage", new { @userId = user.UserId }, TimeSpan.FromDays(27.0));

    return View(user);
}

[AllowAnonymous]
[CallbackAction]
public async Task<ActionResult> SendWelcomeMessage(int userId)
{
    // TODO Validate the parameter

    User user = await DbContext.GetUserAsync(userId);

    // TODO Compose & send the welcome message

    return new EmptyResult();
}

[AllowAnonymous]
[CallbackAction]
public async Task<ActionResult> SendExpirationMessage(int userId)
{
    // TODO Validate the parameter

    User user = await DbContext.GetUserAsync(userId);

    // TODO Compose & send the trial expiration message

    return new EmptyResult();
}

// ...

Whoa! A few things jump out at you. What are those newfangled attributes: [RevaleeClientSettings] and [CallbackAction], and where did the this.CallbackToActionAtAsync() and this.CallbackToActionAfterAsync() methods come from?

Revalee.Client.Mvc

Well, the Revalee project has been enhanced to include a new MVC-specific client library. This library (Revalee.Client.Mvc) is available for download as a NuGet package.

Anyway, let's check out what makes these things tick. We'll start with extension methods.

Extension Methods

For ease of use, a number of Controller extension methods have been added to help expedite the scheduling of Revalee callbacks directly from within controller actions. To add this functionality, a static class was created to encapsulate all of the various static extension methods. The code snippet below lists but a small sampling of the total number of available extension methods, but this should get you started on adding Controller extension methods to your own MVC project:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;

namespace Revalee.Client.Mvc
{
    public static class RevaleeControllerExtensions
    {
        #region Time-based callbacks

        public static Task<Guid> CallbackAtAsync(this Controller controller,
                                                             Uri callbackUri,
                                                  DateTimeOffset callbackTime)
        {
            return SchedulingAgent.RequestCallbackAsync(callbackUri, callbackTime);
        }

        public static Task<Guid> CallbackToActionAtAsync(this Controller controller,
                                                                  string actionName,
                                                                  object routeValues,
                                                          DateTimeOffset callbackTime)
        {
            Uri callbackUri = BuildCallbackUri(
                controller, actionName, null, new RouteValueDictionary(routeValues));

            return CallbackAtAsync(controller, callbackUri, callbackTime);
        }

        // ...more overloads...

        #endregion Time-based callbacks

        #region Delay-based callbacks

        public static Task<Guid> CallbackAfterAsync(this Controller controller,
                                                                Uri callbackUri,
                                                           TimeSpan callbackDelay)
        {
            return SchedulingAgent.RequestCallbackAsync(callbackUri, DateTimeOffset.Now.Add(callbackDelay));
        }

        public static Task<Guid> CallbackToActionAfterAsync(this Controller controller,
                                                                     string actionName,
                                                                     object routeValues,
                                                                   TimeSpan callbackDelay)
        {
            Uri callbackUri = BuildCallbackUri(
                controller, actionName, null, new RouteValueDictionary(routeValues));

            return CallbackAfterAsync(controller, callbackUri, callbackDelay);
        }

        // ...even more overloads...

        #endregion Delay-based callbacks

        #region Uri construction

        private static Uri BuildCallbackUri(Controller controller,
                                                string actionName,
                                                string controllerName,
                                  RouteValueDictionary routeValues)
        {
            string callbackUrlLeftPart = controller.Request.Url.GetLeftPart(UriPartial.Authority);

            RouteValueDictionary mergedRouteValues = MergeRouteValues(
                controller.RouteData.Values, actionName, controllerName, routeValues);

            string callbackUrlRightPart = UrlHelper.GenerateUrl(
                null, null, null, null, null, null,
                mergedRouteValues, RouteTable.Routes, controller.Request.RequestContext, false);

            return new Uri(new Uri(callbackUrlLeftPart, UriKind.Absolute), callbackUrlRightPart);
        }

        private static RouteValueDictionary MergeRouteValues(RouteValueDictionary currentRouteValues,
                                                                           string actionName,
                                                                           string controllerName,
                                                             RouteValueDictionary routeValues)
        {
            if (routeValues == null)
            {
                routeValues = new RouteValueDictionary();
            }

            if (actionName == null)
            {
                object actionValue;

                if (currentRouteValues != null && currentRouteValues.TryGetValue("action", out actionValue))
                {
                    routeValues["action"] = actionValue;
                }
            }
            else
            {
                routeValues["action"] = actionName;
            }

            if (controllerName == null)
            {
                object controllerValue;

                if (currentRouteValues != null && currentRouteValues.TryGetValue("controller", out controllerValue))
                {
                    routeValues["controller"] = controllerValue;
                }
            }
            else
            {
                routeValues["controller"] = controllerName;
            }

            return routeValues;
        }

        #endregion Uri construction
    }
}

All of the methods in the original class (that is, not the abbreviated code snippet above) also include a set of overloads that include the CancellationToken parameter. More specifically, here is an example of such a method from the same class (as above) highlighting the inclusion of that parameter:

public static Task<Guid> CallbackAtAsync(this Controller controller,
                                                     Uri callbackUri,
                                          DateTimeOffset callbackTime,
                                       CancellationToken cancellationToken)
{
    return SchedulingAgent.RequestCallbackAsync(callbackUri, callbackTime, cancellationToken);
}

As can be seen in these examples, many of the highlighted extension methods simply wrap existing static method calls from the internal static class that is called SchedulingAgent. This article won't go into the specifics of that class, but please do not hesitate to download and review the Revalee open source project if you are interested in delving deeper.

Custom Attributes

Before reviewing the specifics of writing a custom attribute class, let's see how one might be used.

SendWelcomeMessage without custom attribute

In the synchronous version of the SendWelcomeMessage() method, the first bits of code in the method validate the callback request:

public ActionResult SendWelcomeMessage(int userId)
{
    if(!RevaleeRegistrar.ValidateCallback(this.Request))
    {
        return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
    }

    // TODO Validate the parameter

    // ...

The thing is, this ValidateCallback() method will have to be copied to every method that is ever called back by Revalee. Duplicating this code again and again can only lead to trouble down the road. What happens if you forget to copy-and-paste this code block? There's got to be a more efficient way to do this.

Well, there is...

SendWelcomeMessage with custom attribute

The code snippet below shows the asynchronous version of the SendWelcomeMessage() method:

[CallbackAction]
public async Task<ActionResult> SendWelcomeMessage(int userId)
{
    // TODO Validate the parameter

    // ...

What's immediately noticeable is the missing ValidateCallback() method code block as well as the presence of a new attribute decorating the method: [CallbackAction]. I wonder where the ValidateCallback() method disappeared to?

Custom Attribute: CallbackActionAttribute

The CallbackActionAttribute, that's where. The class highlighted below encapsulates a single concept: calling the ValidateCallback() method and returning the appropriate error when necessary. As an authorization filter, this code gets called by your Controller class before the body of the action gets called, which makes it a perfect choice for performing validations.

using System;
using System.Web.Mvc;

namespace Revalee.Client.Mvc
{
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    public sealed class CallbackActionAttribute : FilterAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.HttpContext == null
                || filterContext.HttpContext.Request == null
                || !RevaleeRegistrar.ValidateCallback(filterContext.HttpContext.Request))
            {
                filterContext.Result = new HttpUnauthorizedResult();
            }
        }
    }
}

Is that it, you ask? Well, yes. However, the cool thing is that by encapsulating this concept in a custom attribute, callback actions need only be decorated with a simple attribute ([CallbackAction]) instead of having a repetitive code block inserted into them. That's a tidy way of doing things.

Just a final note before anyone raises their hand to point out the obvious: using custom attributes has nothing to do with synchronous versus asynchronous method calls. This particular example just happened to differentiate these two method calls by using a custom attribute. That's all. Moving on.

AttributeTargets Enumeration

Let's look at one more code snippet example to highlight the difference between some of the custom attributes used in the new Revalee.Client.Mvc project. Let's focus on the class declaration, specifically the usage of the AttributeTargets enumeration:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
Inherited = true, AllowMultiple = false)]
public sealed class RevaleeClientSettingsAttribute : FilterAttribute, IActionFilter
{
    // ...

Earlier you saw the CallbackActionAttribute class use the AttributeTargets.Method enumeration, while the RevaleeClientSettingsAttribute class uses both the AttributeTargets.Class and the AttributeTargets.Method enumerations. What this means in practical usage is that the [CallbackAction] attribute may only be used to decorate method declarations, while the [RevaleeClientSettings] attribute may decorate either class or method declarations. Pretty cool, eh?

Conclusion

To summarize, we highlighted some examples of synchronous versus asynchronous methods and how they might be used in an MVC application. Additionally, we reviewed the creation and use of extension methods, in this case used to extend the Controller class. Finally, we introduced custom attributes and how they might be used to encapsulate commonly used code. Hopefully, these examples were helpful to you.

Good coding!

Further Reading

History

  • [2014.Mar.07] Initial post.
  • [2014.May.19] Added 'Further Reading' section.
  • [2014.May.23] Amended 'Further Reading' section with UrlValidator, a Project Widget used by Revalee.

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

László Á. Koller
Software Developer (Senior) Sage Analytic
United States United States
A member of the Sage Analytic software development studio since 2000. Located in Northern NJ. Likes Formula 1, Star Wars, and other things. Enjoys a good laugh.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
QuestionProject management PinmemberMohsin Azam23-May-14 5:42 
QuestionVery good article László Á. Koller! PinprofessionalVolynsky Alex9-Mar-14 14:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140821.2 | Last Updated 23 May 2014
Article Copyright 2014 by László Á. Koller
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid