65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Revalee 和 MVC 安排任务(第二部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (6投票s)

2014年3月7日

MIT

6分钟阅读

viewsIcon

24323

downloadIcon

212

一个在 MVC 控制器中,在长时间延迟后异步发送电子邮件通知的示例。

点击此处阅读本文上一部分:使用 Revalee 和 MVC 进行任务调度

引言

你为自己感到骄傲,承认吧。你的新 MVC 应用程序正在像魔术一样调度 Web 任务,通过 Revalee,尽管 Ted 尽了最大的努力来干扰。 (Ted 是公司里那个总是搞砸事情或根本不做事情的家伙。是的,就是那样的人。) 无论如何,生活很美好。啊哈...

但是等等,泰德为什么走到你的桌子旁?他露出那种笑容。你脖子后面的毛发慢慢竖了起来。“嗯……怎么了,Ted?” (有什么不对劲。) “用户开始抱怨应用程序的性能了。你的应用程序不太好。” 泰德说完就走了,笑得更开心了,他今天的任务完成了,并对自己彻底毁了你的工作充满信心。

ugh。现在怎么办?你错过了什么?

然后,它就像一辆卡车一样击中了你:你的整个 Web 应用程序都使用了同步架构。

背景

等等,什么?你还活在 2000 年吗?(铃铃。“喂?”“是的,你好,现在是 2000 年。我想把我的 Web 应用程序还给我。”)你当然可以在本地测试该应用程序,只有 10 个用户,但现在有多少 10,000 个用户正在访问你的应用程序?大多数时候,你的应用程序的 AppPool 中的线程只是在等待一些 I/O 密集型进程完成。当你的应用程序采用同步架构时,它 simplesmente 无法很好地扩展以处理大量用户,尤其是当你的应用程序包含大量 I/O 密集型进程时。

所以你花时间重新设计你的应用程序以实现完全异步。做得好。但 Revalee 回调呢,你的应用程序调度这些回调?别担心。Revalee 已准备好与你合作。为什么还要让 Revalee 回调阻塞你的工作线程,同时等待其 Web 服务调用完成?

我怎么做到的?你问。嗯,我很高兴你问了……

免责声明: Revalee 是我所属的开发团队编写的一个免费、开源项目。它可以在 GitHub 上找到,并受 MIT 许可证的约束。如果你有兴趣,请下载并查看。

快速回顾

一次,你在 MVC 应用程序中使用了 Revalee 调度了一个同步回调。回顾一下,这是你那个时候 Controller 的一个更详细的代码片段。

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);
}

// ...

这运行得很好。但是,当你需要可伸缩性时,异步处理是最佳选择。因此,在 Ted 破坏了你的好心情之后(正如你可能感觉到的那样,<“shake-fist-at-the-sky”>),你已经重新设计了你的应用程序的大部分以实现这一点。以下是你如何处理你 Web 应用程序中与 Revalee 相关的部分。

Revalee… 异步地

使用上面的代码片段作为你的基准,下面是同一代码片段的异步版本。

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();
}

// ...

哇!有几件事跳入你的眼帘。那些新奇的属性是什么:[RevaleeClientSettings][CallbackAction],还有 this.CallbackToActionAtAsync()this.CallbackToActionAfterAsync() 方法从哪里来的?

Revalee.Client.Mvc

嗯,Revalee 项目已经增强,包含了一个新的、特定于 MVC 的客户端库。这个库(Revalee.Client.Mvc)可以作为 NuGet 包下载。

总之,让我们来看看这些东西是如何工作的。我们将从扩展方法开始。

扩展方法

为了方便使用,添加了许多 Controller 扩展方法,以帮助直接在控制器操作中加快 Revalee 回调的调度。为了添加此功能,创建了一个 static class 来封装各种 static 扩展方法。下面的代码片段只列出了可用扩展方法中的一小部分,但这应该能让你开始向自己的 MVC 项目添加 Controller 扩展方法。

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
    }
}

原始类中的所有方法(也就是说,不是上面那个缩减的代码片段)都包含一组包含 CancellationToken 参数的重载。更具体地说,下面是来自同一 class(如上)的一个方法示例,其中突出了该参数的包含。

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

正如这些示例所示,许多突出显示的扩展方法只是包装了名为 SchedulingAgentinternal static class 中现有的 static 方法调用。本文将不详细介绍该 class 的具体内容,但如果你有兴趣深入了解,请随时 下载并查看 Revalee 开源项目。

自定义属性

在审查编写自定义属性 class 的具体细节之前,让我们看看如何使用它们。

SendWelcomeMessage(无自定义属性)

SendWelcomeMessage() 方法的同步版本中,方法中的第一部分代码会验证回调请求。

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

    // TODO Validate the parameter

    // ...

问题是,这个 ValidateCallback() 方法必须复制到每一个由 Revalee 回调的方法中。一遍又一遍地重复这段代码只会导致未来出现麻烦。如果你忘记复制代码块怎么办?肯定有更有效的方法。

嗯,确实有……

SendWelcomeMessage(带自定义属性)

下面的代码片段显示了 SendWelcomeMessage() 方法的异步版本。

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

    // ...

最明显的是,缺少 ValidateCallback() 方法代码块,并且方法上存在一个新的属性:[CallbackAction]。我想知道 ValidateCallback() 方法去哪儿了?

自定义属性:CallbackActionAttribute

在那里,CallbackActionAttribute。下面突出显示的 class 封装了一个单一的概念:调用 ValidateCallback() 方法并在必要时返回适当的错误。作为一个授权过滤器,这段代码在操作的主体被调用之前会被你的 Controller 类调用,这使其成为执行验证的完美选择。

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();
            }
        }
    }
}

就是这样吗?嗯,是的。但是,很酷的是,通过将这个概念封装在一个自定义属性中,回调操作只需要用一个简单的属性([CallbackAction])来装饰,而不是在它们中插入重复的代码块。这是一种整洁的做法。

在有人举手指出显而易见的问题之前,最后说明一点:使用自定义属性与同步或异步方法调用没有关系。这个特定的例子碰巧通过使用自定义属性来区分这两个方法调用。仅此而已。继续。

AttributeTargets 枚举

让我们来看另一个代码片段示例,以强调新 Revalee.Client.Mvc 项目中使用的一些自定义属性之间的区别。让我们专注于 class 声明,特别是 AttributeTargets 枚举的使用。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
Inherited = true, AllowMultiple = false)]
public sealed class RevaleeClientSettingsAttribute : FilterAttribute, IActionFilter
{
    // ...

之前你看到 CallbackActionAttribute class 使用了 AttributeTargets.Method 枚举,而 RevaleeClientSettingsAttribute class 同时使用了 AttributeTargets.ClassAttributeTargets.Method 枚举。在实际使用中,这意味着 [CallbackAction] 属性只能用于装饰方法声明,而 [RevaleeClientSettings] 属性可以装饰类或方法的声明。很酷,不是吗?

结论

总而言之,我们通过示例强调了同步方法与异步方法以及它们在 MVC 应用程序中的使用方式。此外,我们回顾了扩展方法(在本例中用于扩展 Controller 类)的创建和使用。最后,我们介绍了自定义属性以及如何使用它们来封装常用代码。希望这些例子对你有所帮助。

编码愉快!

延伸阅读

历史

  • [2014.Mar.07] 首次发布。
  • [2014.May.19] 添加了“延伸阅读”部分。
  • [2014.May.23] 修正了“延伸阅读”部分,添加了“UrlValidator,Revalee 使用的一个项目小部件”。
© . All rights reserved.