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






4.91/5 (6投票s)
一个在 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);
}
正如这些示例所示,许多突出显示的扩展方法只是包装了名为 SchedulingAgent
的 internal 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.Class
和 AttributeTargets.Method
枚举。在实际使用中,这意味着 [CallbackAction]
属性只能用于装饰方法声明,而 [RevaleeClientSettings]
属性可以装饰类或方法的声明。很酷,不是吗?
结论
总而言之,我们通过示例强调了同步方法与异步方法以及它们在 MVC 应用程序中的使用方式。此外,我们回顾了扩展方法(在本例中用于扩展 Controller
类)的创建和使用。最后,我们介绍了自定义属性以及如何使用它们来封装常用代码。希望这些例子对你有所帮助。
编码愉快!
延伸阅读
历史
- [2014.Mar.07] 首次发布。
- [2014.May.19] 添加了“延伸阅读”部分。
- [2014.May.23] 修正了“延伸阅读”部分,添加了“UrlValidator,Revalee 使用的一个项目小部件”。