你的位置:首页 > ASP.net教程

[ASP.net教程][翻译+山寨]Hangfire Highlighter Tutorial


前言

Hangfire是一个开源且商业免费使用的工具函数库。可以让你非常容易地在ASP.NET应用(也可以不在ASP.NET应用)中执行多种类型的后台任务,而无需自行定制开发和管理基于Windows Service后台任务执行器。且任务信息可以被持久保存。内置提供集成化的控制台。 原文

Hangfire目前资料不多,官方文档提供两个教程 Sending Mail in Background with ASP.NET MVC 和 Highlighter Tutorial,根据第二个教程山寨了一把,把文中的Entity Framework 改成了 Dapper,Sql Server数据库改成了LocalDB。结合自己的理解把原文翻译一下(第一次翻译,如有不妥尽情拍砖)

Simple sample :https://github.com/odinserj/Hangfire.Highlighter

Full sample:http://highlighter.hangfire.io, sources

 目录 

  • Overview 概述
  • Setting up the project 设置项目
    • Prerequisites  前置条件
    • Creating a project 创建项目
    • Hiliting the code  醒目代码
  • The problem
  • Solving a problem
    • Installing Hangfire
    • Moving to background
  • Conclusion

概述

  考虑到你正在构建一个像GitHub Gists的代码片段库web应用程序,并且想实现语法高亮的特性。为了提供用户体验,甚至你想让它在一个用户禁用JavaScript的浏览器中也能正常工作。

  为了支持这种场景,并且缩短项目开发时间,你会选择使用一个Web Service实现语法高亮,比如:http://pygments.appspot.com or http://www.hilite.me.

Note

Although this feature can be implemented without web services (using different syntax highlighter libraries for .NET), we are using them just to show some pitfalls regarding to their usage in web applications.

You can substitute this example with real-world scenario, like using external SMTP server, another services or even long-running CPU-intensive task. 这段不翻译了,难。

 

设置项目

Tip

This section contains steps to prepare the project. However, if you don’t want to do the boring stuff or if you have problems with project set-up, you can download the tutorial source code and go straight to The problem section.

前置条件

这个教程使用VS2012 (安装了Web Tools 2013 for Visual Studio 2012 补丁),你也可以使用VS2013。这个项目用到了.NET4.5,ASP.NET MVC5 和SQL Server 2008 Express 或更高版本。

注意:那个补丁就是为了在创建项目时有Asp.net MVC5 Empty Project 这个默认的项目模板,我下载了安装报错,找不到对应的包。SqlServer 2008 是为了存储代码片段和Hangfire的任务数据,他还用到了EF做为orm框架。

我这里使用VS2013+LocalDB+Dapper实现类似功能。

创建项目

      打开VS2013->Web->ASP.NET Web 应用程序->弹出框里选择 MVC 模板,身份验证 我改成了 无身份验证

 

创建完成之后默认就有个HomeController.cs 控制器。其中的Index Action 看起来是这样的:

 public class HomeController : Controller  {    public ActionResult Index()    {      return View();    }    public ActionResult About()    {      ViewBag.Message = "Your application description page.";      return View();    }    public ActionResult Contact()    {      ViewBag.Message = "Your contact page.";      return View();    }  }

默认的Index视图也存在了,可以直接F5运行,看看是不是显示了默认页面。

定义模型类(Defining a model )

在Models文件夹下创建CodeSnippet.cs 文件

 1   public class CodeSnippet 2   { 3     public int Id { get; set; } 4  5     [Required, AllowHtml, Display(Name = "C# source")] 6     public string SourceCode { get; set; } 7     public string HighlightedCode { get; set; } 8  9     public DateTime CreatedAt { get; set; }10     public DateTime? HighlightedAt { get; set; }11   }

CodeSnippet

接下来官网文档是安装EntityFramework包,我这里使用整理过的Dapper+DapperExtensions实现的简易ORM(参照地址 ),文件放在Data文件夹下,如:

 

创建一个Services 文件夹,创建 HighLightRepository.cs 文件

 1  /// <summary> 2   /// 仓储类 3   /// </summary> 4   public class HighLightRepository : BaseRepository, IDisposable 5   { 6     public static readonly HighLightRepository Instance = new HighLightRepository(); 7  8     private HighLightRepository() 9       : this(SessionHelper.CreateDefaultSession())10     {11     }12 13     public HighLightRepository(IDBSession dbSession)14       : base(dbSession)15     {16     }17   }18 19   /// <summary>20   /// 数据库连接帮助类21   /// </summary>22   public class SessionHelper23   {24     /// <summary>25     /// 创建默认数据库连接会话26     /// </summary>27     /// <param name="connName"></param>28     /// <returns></returns>29     public static DBSession CreateDefaultSession(string connName = "DefaultConnection")30     {31       var connection = SqlConnectionFactory.CreateSqlConnection(DatabaseType.SqlServer, connName);32 33       return new DBSession(new Database(connection));34     }35   }36 37   /// <summary>38   /// 仓储基类39   /// </summary>40   public abstract class BaseRepository : RepositoryDataImpl, IDisposable41   {42     protected BaseRepository()43     {44       SetDBSession(SessionHelper.CreateDefaultSession());45     }46 47     protected BaseRepository(IDBSession dbSession)48       : base(dbSession)49     {50     }51 52     public void Dispose()53     {54     }55   }

HighLightRepository.cs

这里用到的连接字符串名字是 DefaultConnection。

创建LocalDB数据库文件(Defining a model )

在vs2013中打开SQL Server 对象资源管理器,右键“添加 Sql Server”,Server name 后面输入 (localdb)\v11.0.

 

右键添加数据库ZereoesHangfire,数据库位置 我这里选择了App_Data 文件夹。

新建数据库表CodeSnippet,执行是点击“更新”按钮,弹出一个sql确认框,运行后就可以创建表了,默认用的是dbo架构。

 

Our database is ready to use! 上面这部分没有按照文档中的操作。

创建视图和动作(Creating actions and views)

现在是向我们的项目注入活力的时候了,请按照描述的样子修改文件。

 1 public class HomeController : Controller 2   { 3     public ActionResult Index() 4     { 5  6       var snippetCodeList = HighLightRepository.Instance.GetList<CodeSnippet>(); 7  8       return View(snippetCodeList); 9     }10 11     public ActionResult Details(int id)12     {13       var snippet = HighLightRepository.Instance.GetById<CodeSnippet>(id);14       return View(snippet);15     }16 17     public ActionResult Create()18     {19       return View();20     }21 22     [HttpPost]23     public ActionResult Create([Bind(Include = "SourceCode")] CodeSnippet snippet)24     {25       try26       {27         if (ModelState.IsValid)28         {29           snippet.CreatedAt = DateTime.UtcNow;30           // We'll add the highlighting a bit later.31 32           //方案一:直接调用接口实现高亮33           //using (StackExchange.Profiling.MiniProfiler.StepStatic("Service call"))34           //{35           //  snippet.HighlightedCode = HighlightSource(snippet.SourceCode);36           //  snippet.HighlightedAt = DateTime.Now;37           //}38 39           HighLightRepository.Instance.Insert(snippet);40 41           return RedirectToAction("Details", new { id = snippet.Id });42         }43 44         return View(snippet);45 46       }47       catch (HttpRequestException)48       {49         ModelState.AddModelError("", "Highlighting service returned error. Try again later.");50       }51       return View(snippet);52     }53 54     protected override void Dispose(bool disposing)55     {56       base.Dispose(disposing);57     }58   }

HomeController
 1 @* ~/Views/Home/Index.cshtml *@ 2  3 @model IEnumerable< Zeroes.Snippet.Highlight.Models.CodeSnippet> 4 @{ ViewBag.Title = "Snippets"; } 5  6 <h2>Snippets</h2> 7  8 <p><a class="btn btn-primary" href="@Url.Action("Create")">Create Snippet</a></p> 9 <table class="table">10   <tr>11     <th>Code</th>12     <th>Created At</th>13     <th>Highlighted At</th>14   </tr>15 16   @foreach (var item in Model)17   {18     <tr>19       <td>20         <a href="@Url.Action("Details", new { id = item.Id })">@Html.Raw(item.HighlightedCode)</a>21       </td>22       <td>@item.CreatedAt</td>23       <td>@item.HighlightedAt</td>24     </tr>25   }26 </table>

Index.cshtml
 1 @* ~/Views/Home/Create.cshtml *@ 2  3 @model Zeroes.Snippet.Highlight.Models.CodeSnippet 4 @{ ViewBag.Title = "Create a snippet"; } 5  6 <h2>Create a snippet</h2> 7  8 @using (Html.BeginForm()) 9 {10   @Html.ValidationSummary(true)11 12   <div class="form-group">13     @Html.LabelFor(model => model.SourceCode)14     @Html.ValidationMessageFor(model => model.SourceCode)15     @Html.TextAreaFor(model => model.SourceCode, new { @class = "form-control", style = "min-height: 300px;", autofocus = "true" })16   </div>17 18   <button type="submit" class="btn btn-primary">Create</button>19   <a class="btn btn-default" href="@Url.Action("Index")">Back to List</a>20 }

Create.cshtml
 1 @* ~/Views/Home/Details.cshtml *@ 2  3 @model  Zeroes.Snippet.Highlight.Models.CodeSnippet 4 @{ ViewBag.Title = "Details"; } 5  6 <h2>Snippet <small>#@Model.Id</small></h2> 7  8 <div> 9   <dl class="dl-horizontal">10     <dt>@Html.DisplayNameFor(model => model.CreatedAt)</dt>11     <dd>@Html.DisplayFor(model => model.CreatedAt)</dd>12     <dt>@Html.DisplayNameFor(model => model.HighlightedAt)</dt>13     <dd>@Html.DisplayFor(model => model.HighlightedAt)</dd>14   </dl>15 16   <div class="clearfix"></div>17 </div>18 <div>@Html.Raw(Model.HighlightedCode)</div>

Details.cshtml

 

添加MiniProfiler

Install-Package MiniProfiler,我一般使用界面添加,不在控制台下操作。

安装后,修改Global.asax.cs文件,添加如下代码:

    protected void Application_BeginRequest()    {      StackExchange.Profiling.MiniProfiler.Start();    }    protected void Application_EndRequest()    {      StackExchange.Profiling.MiniProfiler.Stop();    }

修改_Layout.cshtml

<head> <!-- ... --> @StackExchange.Profiling.MiniProfiler.RenderIncludes()</head>

修改web.config

 <!--下面是手动添加的--> <system.webServer>  <handlers>   <add name="MiniProfiler" path="mini-profiler-resources/*" verb="*" type="System.Web.Routing.UrlRoutingModule" resourceType="Unspecified" preCondition="integratedMode" />  </handlers> </system.webServer>

 

醒目(高亮)代码(Hiliting the code)

这是我们应用的和兴功能。我们将使用 http://hilite.me 提供的HTTP API 来完成高亮工作。开始消费这个API之情,先安装  Microsoft.Net.Http 包。

Install-Package Microsoft.Net.Http

这个类库提供简单的异步API用于发送HTTP请求和接收HTTP响应。所以,让我们用它来创建一个HTTP请求到hilite.me 服务。

// ~/Controllers/HomeController.cs/* ... */public class HomeController{  /* ... */  private static async Task<string> HighlightSourceAsync(string source)  {    using (var client = new HttpClient())    {      var response = await client.PostAsync(        @"http://hilite.me/api",        new FormUrlEncodedContent(new Dictionary<string, string>        {          { "lexer", "c#" },          { "style", "vs" },          { "code", source }        }));      response.EnsureSuccessStatusCode();      return await response.Content.ReadAsStringAsync();    }  }  private static string HighlightSource(string source)  {    // Microsoft.Net.Http does not provide synchronous API,    // so we are using wrapper to perform a sync call.    return RunSync(() => HighlightSourceAsync(source));  }  private static TResult RunSync<TResult>(Func<Task<TResult>> func)  {    return Task.Run<Task<TResult>>(func).Unwrap().GetAwaiter().GetResult();  }}

然后,在HomeController.Create 方法中调用它

  [HttpPost]    public ActionResult Create([Bind(Include = "SourceCode")] CodeSnippet snippet)    {      try      {        if (ModelState.IsValid)        {          snippet.CreatedAt = DateTime.UtcNow;          // We'll add the highlighting a bit later.          //方案一:直接调用接口实现高亮          using (StackExchange.Profiling.MiniProfiler.StepStatic("Service call"))          {            snippet.HighlightedCode = HighlightSource(snippet.SourceCode);            snippet.HighlightedAt = DateTime.Now;          }          HighLightRepository.Instance.Insert(snippet);          return RedirectToAction("Details", new { id = snippet.Id });        }        return View(snippet);      }      catch (HttpRequestException)      {        ModelState.AddModelError("", "Highlighting service returned error. Try again later.");      }      return View(snippet);    }

 

Note

We are using synchronous controller action method, although it is recommended to use asynchronous one to make network calls inside ASP.NET request handling logic. As written in the given article, asynchronous actions greatly increase application capacity, but does not help to increase performance. You can test it by yourself with a sample application – there are no differences in using sync or async actions with a single request.

This sample is aimed to show you the problems related to application performance. And sync actions are used only to keep the tutorial simple. 不翻译,难。

 

问题

Tip

You can use the hosted sample to see what’s going on.

现在,运行这个程序,尝试创建一些代码片段,从一个小的开始。你有没有注意到一点延时,当你点击 Create 按钮后?

在我的开发机器上,它看起来用了0.5秒才重定向到详情页面。让我们看看MiniProfiler是什么导致这个延时:

我这里比他好点,用了1.3秒。

像我们看到的,调用web service是我们最大的问题。当我们尝试创建一个稍微大点的代码块时会发生什么?

.......省略两个截图.......

当我们扩大代码片段时,滞后也在增大。此外,考虑到语法高亮Web Service()经历着高负载,或者存在潜在的网络风险。或者考虑到这是CPU密集型任务,你也不能优化。

 Moreover, consider that syntax highlighting web service (that is not under your control) experiences heavy load, or there are latency problems with network on their side. Or consider heavy CPU-intensive task instead of web service call that you can not optimize well.  

Your users will be annoyed with un-responsive application and inadequate delays.

你的用户将对无响应的应用、严重的延时感到愤怒。

解决问题Solving a problem

面对这样的问题你能做什么? Async controller actions 没有什么帮助,像我前面earlier说的。你应该采用某种方法提取Web Service的调用和处理到HTTP请求之外,在后台运行。这里是一些方法:

  • Use recurring tasks and scan un-highlighted snippets on some interval.
  • Use job queues. Your application will enqueue a job, and some external worker threads will listen this queue for new jobs.

Ok, great. But there are several difficulties related to these techniques. The former requires us to set some check interval. Shorter interval can abuse our database, longer interval increases latency.

第一种是轮训没有高亮的片段,第二种是使用一个任务对立。

第一种轮训间隔小了数据库受不了,间隔打了用户受不了。

后一种方法解决了这个问题,但是带来了另外一个问题。队列是否可以持久化?你需要多少工作者线程?怎么协调他们?他们应当工作在哪,ASP.NET 应用内还是外,windows 服务?

最后一个问题是asp.net 应用处理长时间运行请求的痛点。

Warning

DO NOT run long-running processes inside of your ASP.NET application, unless they are prepared to die at any instruction and there is mechanism that can re-run them.

They will be simple aborted on application shutdown, and can be aborted even if the IRegisteredObject interface is used due to time out.

很多问题吧?Relax,你可以使用 Hangfire.它基于持久化队列解决应用程序重启,使用“可靠的获取”捕获意外线程终止,包含协调逻辑运行有多个工作者线程。并且可以足够简单的使用它。

Note

YOU CAN process your long-running jobs with Hangfire inside ASP.NET application – aborted jobs will be restarted automatically.

安装Hangfire (Installing Hangfire)

Install-Package Hangfire

安装完Hangfire,添加或更新OWIN Startup 类。在App_Start 文件夹下添加 Startup.cs 文件

[assembly: OwinStartup(typeof(Zeroes.Snippet.Highlight.App_Start.Startup))]namespace Zeroes.Snippet.Highlight.App_Start{  public class Startup  {    public void Configuration(IAppBuilder app)    {      // 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888      GlobalConfiguration.Configuration.UseSqlServerStorage("DefaultConnection");      app.UseHangfireDashboard();      app.UseHangfireServer();    }  }}

当应用程序第一次启动时会自动创建所有的表。

 

移至后台(Moving to background)

 首先,我们需要定义一个后台任务方法,当工作者线程捕获到高亮任务时调用。我们将简单的定义它为一个静态方法,在 HomeController 中,使用 snippetId  参数。

// ~/Controllers/HomeController.cs/* ... Action methods ... */// Process a job

// Process a job
public static void HighlightSnippet(int snippetId)
{

var snippet = HighLightRepository.Instance.GetById<CodeSnippet>(snippetId);
if (snippet == null) return;

snippet.HighlightedCode = HighlightSource(snippet.SourceCode);
snippet.HighlightedAt = DateTime.UtcNow;

HighLightRepository.Instance.Update(snippet);
}

 

注意这是一个简单的方法,不包含任何Hangfire相关的方法。他调用仓储实例获取一个代码片段,调用Web Service。(这里多线程抵用了HighLightRepository)

然后,我们需要一个地方调用这个方法加入队列。所以,让我们修改 Create 方法:

 [HttpPost]    public ActionResult Create([Bind(Include = "SourceCode")] CodeSnippet snippet)    {      try      {        if (ModelState.IsValid)        {          snippet.CreatedAt = DateTime.UtcNow;          //方案一:直接调用接口实现高亮          //using (StackExchange.Profiling.MiniProfiler.StepStatic("Service call"))          //{          //  snippet.HighlightedCode = HighlightSource(snippet.SourceCode);          //  snippet.HighlightedAt = DateTime.Now;          //}          HighLightRepository.Instance.Insert(snippet);          //方案二:加入任务队列          using (StackExchange.Profiling.MiniProfiler.StepStatic("Job 队列"))          {            // Enqueue a job            BackgroundJob.Enqueue(() => HighlightSnippet(snippet.Id));          }          return RedirectToAction("Details", new { id = snippet.Id });        }        return View(snippet);      }      catch (HttpRequestException)      {        ModelState.AddModelError("", "Highlighting service returned error. Try again later.");      }      return View(snippet);    }    

终于快完了。尝试创建一些代码片段看看时间线吧。(不要担心你会看到空白页,我一会就处理他)

 

 

Good,1.3秒到0.27秒了。但是还存在另外一个问题。你是否注意到有时你重定向的页面内没有任何源码?发生这个是因为我们的视图里包含下面这行:

<div>@Html.Raw(Model.HighlightedCode)</div>

修改Details.cshtml

 1 @* ~/Views/Home/Details.cshtml *@ 2  3 @model  Zeroes.Snippet.Highlight.Models.CodeSnippet 4 @{ ViewBag.Title = "Details"; } 5  6 <h2>Snippet <small>#@Model.Id</small></h2> 7  8 <div> 9   <dl class="dl-horizontal">10     <dt>@Html.DisplayNameFor(model => model.CreatedAt)</dt>11     <dd>@Html.DisplayFor(model => model.CreatedAt)</dd>12     <dt>@Html.DisplayNameFor(model => model.HighlightedAt)</dt>13     <dd>@Html.DisplayFor(model => model.HighlightedAt)</dd>14   </dl>15 16   <div class="clearfix"></div>17 </div>18 @*方案一*@19 @*<div>@Html.Raw(Model.HighlightedCode)</div>*@20 21 @*方案二*@22 @if (Model.HighlightedCode == null)23 {24   <div class="alert alert-info">25     <h4>Highlighted code is not available yet.</h4>26     <p>27       Don't worry, it will be highlighted even in case of a disaster28       (if we implement failover strategies for our job storage).29     </p>30     <p>31       <a href="javascript:window.location.reload()">Reload the page</a>32       manually to ensure your code is highlighted.33     </p>34   </div>35 36   @Model.SourceCode37 }38 else39 {40   @Html.Raw(Model.HighlightedCode)41 }

Details.cshtml

另外你需要拉取你的应用程序使用AJAX,直到它返回高亮代码。

 1     //获取高亮代码 2     public ActionResult HighlightedCode(int snippetId) 3     { 4       var snippet = HighLightRepository.Instance.GetById<CodeSnippet>(snippetId); 5       if (snippet.HighlightedCode == null) 6       { 7         return new HttpStatusCodeResult(HttpStatusCode.NoContent); 8       } 9       return Content(snippet.HighlightedCode);10     }

HighlightedCode

Or you can also use send a command to users via SignalR channel from your HighlightSnippet method. But that’s another story.

Note

Please, note that user still waits until its source code will be highlighted. But the application itself became more responsive and he is able to do another things while background job is processed.

总结

In this tutorial you’ve seen that:

  • Sometimes you can’t avoid long-running methods in ASP.NET applications.
  • Long running methods can cause your application to be un-responsible from the users point of view.
  • To remove waits you should place your long-running method invocation into background job.
  • Background job processing is complex itself, but simple with Hangfire.
  • You can process background jobs even inside ASP.NET applications with Hangfire.

翻译、贴代码用了快3个小时,现在都快吐了。如果您觉着有点用处,就点个赞吧、赞吧、赞吧!

希望更多的人一起学习这个组件,下载地址(稍后补上,CSDN上传太慢了)

补两个界面: