在 asp.net mvc5 中执行长时间运行任务的最佳方法

joh*_* Gu 4 asp.net iis asp.net-mvc quartz.net asp.net-mvc-5

我正在开发一个部署在 IIS 7.0 中的 asp.net mvc5 web 应用程序 + 实体框架 6.0。目前我有一个 NetworkScanning 服务器,我将其实现为正常操作方法,可以通过两种方式启动:-

1.基于 global.asax 中定义的时间表:-

static void ScheduleTaskTrigger()
        {
            HttpRuntime.Cache.Add("ScheduledTaskTrigger",
                                  string.Empty,
                                  null,
                                  Cache.NoAbsoluteExpiration,
                                  TimeSpan.FromMinutes(Double.Parse(System.Web.Configuration.WebConfigurationManager.AppSettings["ScanInMinutes"])), // as defined inside the web.config
                                  CacheItemPriority.NotRemovable,
                                  new CacheItemRemovedCallback(PerformScheduledTasks));
        }

        static void PerformScheduledTasks(string key, Object value, CacheItemRemovedReason reason)
        {
            //Your TODO
            HomeController h = new HomeController();
            var c = h.ScanNetwork("***", "allscan");
            ScheduleTaskTrigger();
        }
Run Code Online (Sandbox Code Playgroud)

2.或者从用户浏览器手动调用action方法。

现在 action 方法看起来如下(我删除了很多代码,因为这个想法是为了提示我在 action 方法中所做的事情):-

public async Task<ActionResult> ScanNetwork(string token, string FQDN) 
        {
            string TToken = System.Web.Configuration.WebConfigurationManager.AppSettings["TToken"];//get the T token from the web.config, this should be encrypted
            var cccc = Request == null ? System.Web.Configuration.WebConfigurationManager.AppSettings["TIP"] : Request.UserHostAddress;
            if (token != TToken || cccc != System.Web.Configuration.WebConfigurationManager.AppSettings["TIP"]) 
            {


                if (FQDN != "allscan")
                { return Json(new { status = "fail", message = "Authintication failed." }, JsonRequestBehavior.AllowGet); }
                return new HttpStatusCodeResult(403, "request failed");
            }

            try
            {

              scaninfo = await repository.populateScanInfo(false); // get the info for all the servers from the database



                using (WebClient wc = new WebClient()) 
                {
                    string url = currentURL + "resources?AUTHTOKEN=" + pmtoken;
                    var json = await wc.DownloadStringTaskAsync(url);
                    resourcesinfo = JsonConvert.DeserializeObject<ResourcesInfo>(json);
                }


                foreach (var c in scaninfo) //loop through all the hypervisot server/s
                {


                    if (passwordmanagerResource.Count() == 0) // if there is not any record for the resource on the password manager
                    {


                       await repository.Save();
                        continue;

                    }
                    else if (passwordmanagerResource.Count() > 1) // if more than on record is defined for the same resource on PM
                    {


                        await repository.Save();
                        continue;

                    }
                    else
                    {



                        using (WebClient wc = new WebClient()) // call the PM API to get the account id 
                        {
                            string url = currentURL + "resources/" + passwordmanagerResourceID + "/accounts?AUTHTOKEN=" + pmtoken;
                            var json = await wc.DownloadStringTaskAsync(url);

                            resourceAccountListInfo = JsonConvert.DeserializeObject<ResourceAccountListInfo>(json);
                        }

                        using (WebClient wc = new WebClient()) // call the PM API to get the password for the account id under the related resource
                        {
                            string url = currentURL + "resources/" + passwordmanagerResourceID + "/accounts/" + passwordmanagerAccountID + "/password?AUTHTOKEN=" + pmtoken;
                            var json = await wc.DownloadStringTaskAsync(url);
                            resourceAccountPasswordListInfo = JsonConvert.DeserializeObject<ResourceAccountPasswordInfo>(json);
                        }

                        var shell = PowerShell.Create();
                        var shell2 = PowerShell.Create();
                        var shell3 = PowerShell.Create();

                        //Powercli script to get the hypervisot info
                        string PsCmd = "add-pssnapin VMware.VimAutomation.Core; $vCenterServer = '" + vCenterName + "';$vCenterAdmin = '" + vCenterUsername + "' ;$vCenterPassword = '" + vCenterPassword + "';" + System.Environment.NewLine;



                        PsCmd = PsCmd + "$VIServer = Connect-VIServer -Server $vCenterServer -User $vCenterAdmin -Password $vCenterPassword;" + System.Environment.NewLine;



                        PsCmd = PsCmd + "Get-VMHost " + System.Environment.NewLine;

                        string PsCmd2 = "add-pssnapin VMware.VimAutomation.Core; $vCenterServer = '" + vCenterName + "';$vCenterAdmin = '" + vCenterUsername + "' ;$vCenterPassword = '" + vCenterPassword + "';" + System.Environment.NewLine;



                        PsCmd2 = PsCmd2 + "$VIServer = Connect-VIServer -Server $vCenterServer -User $vCenterAdmin -Password $vCenterPassword;" + System.Environment.NewLine;



                        PsCmd2 = PsCmd2 + " Get-VMHost " + vCenterName + "| Get-VMHostNetworkAdapter -VMKernel" + System.Environment.NewLine;



                        shell.Commands.AddScript(PsCmd);
                        shell2.Commands.AddScript(PsCmd2);
                        dynamic results = shell.Invoke(); // execute the first powercli script
                        dynamic results2 = shell2.Invoke();//execute the second powercli script


                        if (results != null && results.Count > 0 && results[0].BaseObject != null) // the powercli executed successfully
                        {
                            // call the service desk API to update the hypervisor info
                            var builder = new StringBuilder();

                            XmlDocument doc = new XmlDocument();
                            using (var client = new WebClient())
                            {

                                var query = HttpUtility.ParseQueryString(string.Empty);


                              //code goes here

                                string xml = await client.DownloadStringTaskAsync(url.ToString());

                                doc.LoadXml(xml);
                                status = doc.SelectSingleNode("/operation/operationstatus").InnerText;
                                message = doc.SelectSingleNode("/operation/message").InnerText;


                            }

                        else//if the powershell script return zero result..
                        {

                            c.TServer.ScanResult = "Scan return zero result";
                            scan.Description = scan.Description + "<span style='color:red'>" + c.TServer.ScanResult + "</span><br/>";
                            await repository.Save();
                            continue;
                        }
                        if (FQDN == "allscan")
                        {

                           //code goes here
            }
            catch (WebException ex)
            {
                errormessage = "Password manager or manage engine can not be accessed";
                errorstatus = "fail";

            }
            catch (Exception e)
            {
                errormessage = "scan can not be completed. Message" + e.InnerException;
                errorstatus = "fail";

            }
            scan.EndDate = System.DateTime.Now;


                using (MailMessage mail = new MailMessage(from, "*****"))
                {
                    mail.Subject = "scan report generated";
                    //  mail.Body = emailbody;

                    mail.IsBodyHtml = true;
                    System.Text.StringBuilder mailBody = new System.Text.StringBuilder();
                    mailBody.AppendLine("<span style='font-family:Segoe UI'>Hi, <br/>");
                    mailBody.AppendLine(scan.Description);
                    mailBody.AppendLine("<br/><br/><div style='color:#f99406;font-weight:bold'>T scanning Management </div> <br/> <div style='color:#f99406;font-weight:bold'>Best Regards</div></span>");

                    SmtpClient smtp = new SmtpClient();
                    smtp.Host = System.Web.Configuration.WebConfigurationManager.AppSettings["smtpIP"];
                    smtp.EnableSsl = true;
                    mail.Body = mailBody.ToString();

                    smtp.UseDefaultCredentials = false;

                    smtp.Port = Int32.Parse(System.Web.Configuration.WebConfigurationManager.AppSettings["smtpPort"]);
                    S
                    smtp.Send(mail);
                }
            }
            return Json(new { status = errorstatus, message = errormessage }, JsonRequestBehavior.AllowGet);



        }
Run Code Online (Sandbox Code Playgroud)

现在如上面的操作方法所示,它包含许多操作,例如;从 DB 检索对象,保存 DB 更改,在第三方应用程序上调用 Web 客户端,运行 powercli 脚本等。时间。但是从我自己的阅读来看,运行诸如上述操作方法之类的长时间运行的任务被认为是有风险的,我需要使用不同的方法。那么任何人都可以提出为什么上述方法被认为有风险以及我如何改进它?第二个问题。现在我读到 Quartz.NET 是一种可以遵循的方法,但不确定我是否仍然可以从 Web 浏览器调用 Quartz.NET 方法并且不确定我是否可以在 Quartz.NET 中调用 webclient、执行 powercli 脚本等?

谢谢

Lef*_*tyX 5

正如有人建议 Quartz.Net 的替代品可以是Hangfire
最后一个比 Quartz.Net 更容易实现,它有一个漂亮的管理仪表板。

Hangfire 为您提供与 Quartz.Net 几乎相同的功能。

您可以在 Owin Startup 中引导它:

var options = new SqlServerStorageOptions
{
    PrepareSchemaIfNecessary = true,
    QueuePollInterval = TimeSpan.FromSeconds(15)
};

GlobalConfiguration.Configuration
    .UseSqlServerStorage("<your connection string here>", options)
    .UseNLogLogProvider()
    .UseActivator(new StructureMapHangfireJobActivator(My.Application.Container));

app.UseHangfireServer();

app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    AuthorizationFilters = new[] { new BpNetworkSales.Web.Infrastructure.ActionFilters.HangfireDashboardAuthorizationFilter() }
});
Run Code Online (Sandbox Code Playgroud)

您可以使用您喜欢的记录器:

GlobalConfiguration.Configuration.UseNLogLogProvider();
Run Code Online (Sandbox Code Playgroud)

你可以使用你最喜欢的 DI 容器:

GlobalConfiguration.Configuration.UseActivator(new StructureMapHangfireJobActivator(My.Application.Container));
Run Code Online (Sandbox Code Playgroud)

并且您可以轻松运行重复任务:

RecurringJob.AddOrUpdate("RunSyncDocumentsForStatus", () => My.Application.SyncDocumentsForStatus(My.Application.CompanyCode), "0/3 * * * *");
Run Code Online (Sandbox Code Playgroud)

安排延迟的工作:

Hangfire.BackgroundJob.Schedule(() => My.Application.SyncSubmittedDocuments(), TimeSpan.FromSeconds(60));
Run Code Online (Sandbox Code Playgroud)

并使用属性自动重试操作:

[Hangfire.AutomaticRetry(Attempts = 5)]
public static void SyncSubmittedDocuments()
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

或禁用并发执行:

[Hangfire.DisableConcurrentExecution(timeoutInSeconds: 120)]
public static void SyncDocumentsForStatus(string companyCode)
{

}
Run Code Online (Sandbox Code Playgroud)

它只是一个设计精美的软件;而 Quartz.Net 有点复杂,尤其是当你开始做“花哨的”东西时。

您必须始终记住,您是在 IIS 内运行这些任务,因此当 IIS 挂起或回收时,您的作业将关闭。

在您之前的问题中,Jay Vilalta(他对 Quartz.Net 非常了解)告诉您替代方案是使用 Quartz.Net 作为 Windows 服务。

我想如果您计划安排要在特定时间运行的长期重复性作业,并且您希望 100% 确定它们将被执行,那么 Windows 服务是您拥有的最佳选择。

另一个优点是您的 ASP.NET MVC 应用程序将更流畅,因为您不会有其他后台任务,而这些任务将在用户执行日常工作时运行。

如果您不熟悉 Windows 服务,我建议您选择Topshelf,它集成了 Quartz.Net。