[转]整合Web和Windows服务——按预定时间间隔运行ASP.NET代码

作者:Andrew Needleman
相关技术:C#、.NET、ASP.NET、Window
难度:★★★★☆
读者类型:数据库开发人员、系统结构设计人员

    [摘要]本文讨论了如何安排ASP.NET代码的运行和N层体系结构设计,介绍了Web服务和Windows服务基础知识。

    由于Web服务可以与ASP.NET应用程序的其余部分在相同的应用程序上下文中运行,因此它可以在现有代码所预期的相同上下文中执行。因为Windows服务可以在Windows启动时自行启动,所以我将通过Windows服务来启动Web服务调用。

    假设您已经用ASP.NET编写了一个出色的N层应用程序,并且想要扩展它以执行预定任务。例如,每两个小时向数据库中的选定用户发送电子邮件,或者定期分析ASP.NET缓存中的数据以进行应用程序运行状况的监视。您不希望从ASP.NET应用程序中丢弃您的对象模型或者在单独的计划程序和ASP.NET应用程序之间创建太多的依赖项,那么您如何在避免这一点的同时仍然能够让这些应用程序一起工作呢?在基于.NET Framework的应用程序中,经常使用计时器按照预定时间间隔执行活动,因此使用一个计时器似乎是适当的解决方案。您可以从Global.asax中的 Application_Start处理程序中启动计时器以运行预定任务。遗憾的是,该解决方案在应用程序域、进程或系统重新启动方面还不够健壮,这是因为必须向应用程序发出请求以启动计时器。ASP.NET是一种只响应HTTP请求的被动编程范型,因此进程或用户输入必须调用代码以便它能够运行。

    更好的解决方案是使用Web服务(Web Service)提供ASP.NET应用程序的接口,并且生成按照预定时间间隔调用它的Windows服务。这样,ASP.NET应用程序就不必拥有日程安排逻辑,并且只需要关心执行那些它已经能够执行的任务。由于Web服务可以与ASP.NET应用程序的其余部分在相同的应用程序上下文中运行,因此它可以在现有代码所预期的相同上下文中执行。因为Windows服务可以在Windows启动时自行启动,所以我将通过Windows服务来启动Web服务调用。因此,即使服务器重新启动,应用程序也能够启动自身。这一重新启动功能使Windows服务成为对该任务而言比典型的基于Windows的应用程序更为健壮的解决方案。这也是为什么Windows服务能用于很多后台进程(例如IIS)的原因。

    本文,我将演示如何做到这一点,同时在日程安排应用程序和ASP.NET应用程序之间创建最少数量的依赖项。该解决方案涉及到对启动ASP.NET作业的日程安排应用程序进行简化。在日程安排应用程序中,除了它调用的Web服务终结点以外,将不会调用特定于ASP.NET应用程序的逻辑。Windows服务将使用app.config文件来存储Web服务的UR 以及Windows服务在对Web服务进行的调用之间应当等待的时间间隔。通过在Windows服务的app.config文件中存储这两个设置,您可以更改它们,而无须重新编译Windows服务。如果您需要在应用程序调用时更改它的行为,则可以只更改ASP.NET应用程序中的逻辑;但是,您不必更改日程安排应用程序的代码。这意味着日程安排应用程序将与ASP.NET应用程序中的更改隔离。

    注意:该解决方案所基于的前提是——有一些任务只应当在正在运行的ASP.NET应用程序的上下文中执行。如果这不是您任务的要求,则您应当认真考虑直接从 Windows服务中引用ASP.NET应用程序的业务逻辑程序集,并且绕过ASP.NET进程以激发这些任务。

应用程序结构


图1 计划

    典型的ASP.NET应用程序是用一系列执行特定功能的独立层生成的。在我的特定示例中,我具有数据库访问类、业务逻辑类、业务流程类以及作为这些层的入口点的ASP.NET页(参见图 1)。

    ASP.NET页只用来显示和检索数据,它们是实际协调所有工作的业务流程类的接口。流程类按照正确的顺序调用业务逻辑类,以便完成特定的事务,例如订购小部件。例如,流程类可以首先调用业务逻辑以检查库存,然后订购小部件,并且最终将库存减少至适当的水平。

    业务逻辑类决定如何调用数据库访问类,并且根据需要处理该结果,以获得可以用于其他操作的最终结果。例如,业务逻辑可以用来计算包括特定州的税款在内的总价格。首先,您可能需要使用数据访问类从数据库中检索该州的税率以及基本价格,然后将它们相乘以查找每个项的总税款。数据库访问类保持该逻辑,以便连接到数据库,并且以可供更高层使用的格式(例如,DataSet、DataTable或DataReader)返回结果集。这些类只是从数据库中检索数据,并且按照反馈给它们的信息来更新该数据库;它们不处理结果。例如,它们可能检索特定州(美国)的税率,但是它们不会计算订单的总税款。

    Microsoft Data Access Application Building Block通过提供更容易的方式与数据库和存储过程进行通信简化了数据访问类。例如,您可以对它的SQLHelper对象的FillDataSet方法进行调用,以便使用一行代码根据存储过程的输出来填充DataSet。通常,您必须编写代码来至少创建DataAdapter和一个命令对象,而这至少需要四行代码。Data Access Application Block连接到数据库中的存储过程。该存储过程提供访问和修改数据库中的数据所需要的SQL代码。

向应用程序中添加预定作业

    ASP.NET Web服务能够提供保持任务逻辑的现有ASP.NET应用程序的接口,该接口充当该服务和调用ASP.NET应用程序以采取操作的Windows服务之间的中间人。Windows服务随后将按照预定时间间隔调用ASP.NET应用程序。


图2 运行预定作业

    ASP.NET Web服务能够向您提供保持任务逻辑的现有ASP.NET应用程序的接口。该接口充当该服务和调用ASP.NET应用程序以采取操作的Windows服务之间的中间人。Windows服务随后将按照预定时间间隔调用ASP.NET应用程序。通过在现有ASP.NET应用程序中生成ASP.NET Web服务,可以在预定作业中重新使用已经为该ASP.NET应用程序创建的业务对象和逻辑。图2显示应用程序流程的详细信息——从客户端Windows服务应用程序到在该服务器上运行的Web服务启动,始终贯穿于每个预定任务的执行。


图3 应用程序附加功能

    正如您可以在图3中看到的那样,该过程要求对前面描述的标准分层进行一些修改。Windows服务将按照指定的时间间隔唤醒ASP.NET Web服务。然后,ASP.NET Web服务将调用Web应用程序流程层中的方法,以实际确定应当运行哪些预定作业,并随后运行它们。在实现了基本解决方案以后,您可以使用客户端app.config文件来确定Windows服务调用Web服务的时间间隔。接下来,您可以添加业务流程层所需要的功能,以便遍历和运行作业。许多N层专家对于流程层的兴趣肯定超过了对其余层的兴趣,因此我将最后讨论数据库表、数据库存储过程、数据访问代码和业务逻辑。最后,从底部(数据库表级别)到中部(业务逻辑层)向应用程序的现有层中添加代码,以便支持流程层所使用的作业功能。

生成Web服务

    要生成Web服务,请首先向与现有ASP.NET代码位于相同层中的ASP.NET应用程序中添加JobRun ASP.NET Web服务。确保您的ASP.NET项目具有对业务逻辑、流程和数据访问项目的引用。接下来,要在JobRun Web服务中创建RunJob Web服务方法,Web服务方法将需要调用流程层的相应函数以运行适当的作业。这意味着RunJob方法可以像下面一样简单:

[WebMethod]
public void RunJob()
{
    Flow.JobFlow jf = new Flow.JobFlow();
    jf.RunAllActiveJobs();
}

    使用RunJob函数创建JobFlow类(它位于流程层中)的实例并调用它的RunAllActiveJobs函数。JobFlow函数的RunAllActiveJobs完成了协调作业运行的所有实际工作,而RunJob函数只是充当该序列的入口点。请注意,这段代码无法防止作业同时在一个以上的线程中运行——如果Windows服务过于频繁地安排任务(速度超过了任务的运行速度),或者如果其他某个应用程序调用了入口点,则可能发生这种情况。如果该方法不是线程安全的并且允许多个线程同时调用它,则可能给这些作业的结果带来问题。例如,如果作业X向Mary Smith发送了一封电子邮件,但是在作业Y查询数据库以处理其电子邮件时尚未更新数据库,则Mary可能收到两封电子邮件。为了同步对该函数的访问,我将使用System.Threading命名空间中的Mutex类:

private static Mutex mut = new Mutex(false, "JobSchedulerMutex");

    Mutex支持跨进程同步,因此这可以防止多个进程同时运行——即使涉及到两个不同的ASP.NET辅助进程。现在,让我们更改RunJob方法以使用Mutex,从而确保在启动作业之前没有其他作业运行。

代码段1 RunJob Web Service:

[WebMethod]
public bool RunJob()
{
    bool ranJob = false;
    mut.WaitOne();
    try
    {
        Flow.JobFlow jf = new Flow.JobFlow();
        jf.RunAllActiveJobs();
        ranJob = true;
    }
    finally
    {
        mut.ReleaseMutex();
    }
    return ranJob;
}

    正如您可以在代码段1中的RunJob函数中看到的那样,可以调用Mutex的WaitOne函数以使该线程等待,直到它在执行之前成为唯一的线程。然后,调用ReleaseMutex函数以指示您已经执行完只需要在一个线程中运行的代码。当然,在这里阻塞可能不是正确的解决方案。您可以选择在另一个线程已经执行作业时(在此情况下,您可以为WaitOne方法指定短暂的超时)立即返回,并且在无法获得互斥锁时立即从RunJob中返回。将该函数的所有主要操作都放到一个try-finally块中,以便即使RunAllActiveJobs函数中的意外异常导致RunJob函数退出,也会调用ReleaseMutex。

    您可能希望使用某种形式的身份验证和授权(可能使用Windows安全)来保证 Web服务的安全,以确保必须经过正确的授权才能运行该服务,但是我不打算在本文对此进行详细讨论。

    既然您已经生成了Web服务以便可以从另一个应用程序中调用它,那么现在就让我们生成能够使用它的Windows服务。

生成Windows服务

    首先, 在Visual Studio .NET的另一个实例中创建一个新的Windows服务项目,然后将其命名为InvokingASPNetService.cs。通过按以下方式添加Main方法来确保该服务将正确启动:

public static void Main()
{
    ServiceBase.Run(new InvokingASPNetService());
}

    现在,为下列命名空间添加using语句:

using System.Configuration;
using System.Globalization;

    通过右键单击InvokingASPNetService.cs的设计表面并选择“Add Installer”,添加该服务的安装程序。您应当将所创建的serviceInstaller1 的StartType属性更改为Automatic,以便Windows服务在Windows启动时启动。将serviceInstaller1 的 ServiceName 属性设置为InvokingASPNetService,以便在服务管理器中适当地命名它,然后将serviceProcessInstaller1 Account 属性更改为Local Service。

    第三步,创建对InvokingASPNetService Web服务的Web引用,然后将其命名为JobRunWebService。将JobRunWebService URL Behavior属性更改为Dynamic,以便让Visual Studio .NET自动用Web引用的URL来扩充app.config。所生成的代理类将在该配置文件中查找Web服务的URL,从而使您无须重新编译即可将 Windows服务引导至不同的终结点。

    第四,在Windows服务中创建一个方法,以便在Web服务每次被调用时运行它。该方法如下所示:

private void RunCommands()
{
    JobRunWebService.JobRunInterval objJob =
        new JobRunWebService.JobRunInterval();
    objJob.RunJob();
}

    如您所见,您将声明Web服务代理,并且就像创建其他任何.NET对象一样创建它。然后,调用Web服务的RunJob方法,以便在远程Web服务器上运行作业。请注意,每个步骤都与使用本地类没有什么不同,即使您使用的是Web服务。

    第五,您需要在Windows服务中调用RunCommands函数。您应当基于您希望在远程服务器上运行作业的频率,按照固定的时间间隔调用该方法。使用System.Timers.Timer对象来确保RunCommands函数按照正确的时间间隔运行。Timer的Elapsed事件将使您可以在每个时间间隔流逝之后触发您指定的任何函数(请注意,时间间隔长度是在Interval属性中指定的)。您将使用被触发的函数来调用RunCommands函数,以便可以自动执行该功能。默认情况下,该timer类只在它首次到期时才会触发事件,因此您需要通过将它的AutoReset属性设置为true来确保它每次都反复地重置它本身。您应当在服务级别对其进行声明,以便该服务的任何函数都可以引用它:

private Timer timer;

    接下来,创建相应的函数以初始化timer并设置它的所有相关值:

private void InitializeTimer()
{
    if (timer == null)
    {
        timer = new Timer();
        timer.AutoReset = true;
        timer.Interval = 60000 * Convert.ToDouble(
            ConfigurationSettings.AppSettings["IntervalMinutes"]);
        timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
    }
}

    为了能够在不重新编译应用程序的情况下更改配置时间间隔,我已经在app.config文件中存储了该时间间隔,以便InitializeTimer方法可以使用ConfigurationSettings.AppSettings访问它而不是对其进行硬编码,如下所示:

<add key="IntervalMinutes" value="5" />

    确保timer在计时器到期时调用timer_Elapsed函数以处理Elapsed事件。timer_Elapsed方法非常简单,它调用刚刚生成的RunCommands函数,如下所示:

private void timer_Elapsed(object source,System.Timers.ElapsedEventArgs e)
{
    RunCommands();
}

    最后,您必须使用installutil命令安装Windows服务。最容易的方式是打开Visual Studio .NET命令提示窗口,导航到该服务的目录,然后运行installutil实用工具,并且指定您的程序集作为参数。

扩展流程层以处理预定作业

    一项重要的工作是扩展流程层以处理正在运行的预定作业的需要(假定作业之间的差异足够大,以至于需要对它们进行编码而不仅仅是参数化)。这涉及到从数据库中的下一个启动时间已经过去的数据库收集所有作业,并单独地运行它们。在流程层内部,您将创建一个名为Job的基类以提供作业所共有的全部功能。这包括一个用于初始化和检索JobID的机制、一个用于运行作业以及在成功运行之后设置数据库中的下一个运行的公共方法(RunSingleJob),以及一个要针对每个作业进行自定义的可重写方法(PerformRunJob)。

    流程层还将需要为它执行的每个作业生成特定于作业的类。这些类将从基础的Job类继承,并将重写Job类的PerformRunJob函数以自定义该特定作业的执行。您还会需要一个工厂类(JobFactory)以创建和初始化正确的Job类的JobID。静态的CreateJob函数将根据传递到该函数中的JobID来创建适当的作业。最后,流程层将必须能够确定需要运行的作业、遍历它们并运行它们。这就是JobFlow类将通过它的RunAllActiveJobs方法提供的功能。

    首先,让我们在流程层项目中创建Job基类(该类将成为每个作业类的父类)。代码段2显示Job抽象基类的核心。它允许对其JobID进行初始化和检索,并且能够确保在作业成功运行后更新数据库。JobID在创建之后,它将不会针对给定的作业进行更改,因此您必须确保在初始化之后设置函数不会更改该值。创建各个Job类的JobFactory类将设置自己的JobID值。

代码段2 Job抽象基类:

protected bool isInitialized = false;
protected int mJobID;
public int JobID
{
    get { return mJobID; }
    set
    {
        if (!isInitialized)
        {
            mJobID = value;
            isInitialized = true;
        }
        else throw new InvalidOperationException("JobID already set.");
    }
}

public void RunSingleJob()
{
    if (isInitialized)
    {
        PerformRunJob();
        RecordJobSuccess();
    }
}

protected abstract void PerformRunJob();

protected void RecordJobSuccess()
{
    JobLogic jl = new JobLogic();
    jl.UpdateJobDone(JobID);
}

    RunSingleJob函数确定该作业的JobID已经初始化,运行作业(PerformRunJob),并且在成功运行之后用RecordJobSuccess方法更新数据库。isInitialized变量用来确保每个作业在运行之前都使它的JobID得到初始化。PerformRunJob抽象方法由派生的Job类实现,并且保持任务的实际逻辑。在作业的实现(PerformRunJob方法)成功运行之后,基类调用 RecordJobSuccess函数,该函数使用业务逻辑层的JobLogic类的UpdateJobDone方法来记录它在数据库中运行的时间以及预定的下一个运行时间。稍后,我将创建业务逻辑层的JobLogic类。

    Job类既提供初始化JobID变量的功能,又提供在成功时用下一个运行时间更新数据库的功能。另外,您只须用特定于类的代码重写一个函数。这使您可以创建Job类的子类。为此,您需要创建两个类,以便运行特定类型的作业,并且从Job类继承以获得它们的其余功能。创建一个JobRunTest类和一个JobEmailUsers类,并且确保每个类都从Job类继承,如下所示:

public class JobRunTests : Job

    现在,按如下方式重写这两个类的PerformRunJob方法(使用JobRunTest类作为示例):

protected override void PerformRunJob()
{
    ///Do RunTest specific logic here
}

    将特定于作业的逻辑放到该方法的内部。负责运行作业并且更新数据库中下一个运行时间的其余代码是从Job基类中继承的。您的作业将组合对现有业务逻辑类的调用,以便运行复杂的过程。既然已经有了示例作业,那么让我们考察一下如何使用JobFactory对象创建这些作业。

    JobFactory类用于创建每个JobID的相应子Job类。JobFactory类在它的静态 CreateJob函数中采用JobID变量,并且返回适当的Job子类。代码段3显示JobFactory中的代码。

代码段3 JobFactory类:

public static Job CreateJob(int currentJobID)
{
    Job myJob;
    switch(currentJobID)
    {
        case 1:
            myJob = new JobEmailUsers();
            break;
        case 2:
            myJob = new JobRunTest();
            break;
        default:
            return null;
    }
    myJob.JobID = currentJobID;
    return myJob;
}

    CreateJob函数采用当前JobID,并且在一个case语句中用它来确定应当返回Job类的哪个子类。然后,初始化当前的JobID并且返回从Job派生的类。既然您具有了Job基类、它的特定于作业的子类以及用来选择要创建的类的方式,那么您就可以考察如何使用JobFlow类将这一切组合在一起。

    要创建一个名为JobFlow的类以便收集和执行适当的作业,请添加一个名为“RunAllActiveJobs”的函数以遍历您需要运行的每个作业,并调用它们各自的RunSingleJob函数。您将需要使用RunAllActiveJobs函数来获取预定从数据库中经过业务层、数据访问层和存储过程运行的作业的列表,然后使用其各自的RunSingleJob函数来运行它们。以下代码显示JobFlow类的RunAllActiveJobs方法如何完成这些目标:

JobLogic jl = new JobLogic();
DataSet jobsActiveData = jl.GetAllActiveJobs();
foreach (DataRow jobsActive in jobsActiveData.Tables[0].Rows)
{
    int currentJobID = Convert.ToInt32(jobsActive["JobID"]);
    Job myJob = JobFactory.CreateJob(currentJobID);
    myJob.RunSingleJob();
}

    基本上,您将在数据库中存储作业,同时存储有关它们上次运行的时间以及代码在连续两次运行之间应当等待的时间间隔的信息。然后,通过BusinessLogic层中带有GetAllActiveJobs方法的JobLogic类来检索需要运行的作业。每个活动作业的ID都用于获得Job对象,如前所述,该对象的RunSingleJob方法可以用来执行任务。

作业计时信息

    确定应当运行哪些预定作业意味着您需要存储有关它们的基本信息,例如,连续两次运行之间的时间间隔、它们的上次运行时间和它们下一次应当运行的时间。为了完成该工作,请在SQL Server数据库中创建一个作业表(参见表1)。

表1 Job表

Column Datatype
JobID int identity
JobTitle varchar(500)
JobInterval datetime
DateLastJobRan datetime
DateNextJobStart datetime

    JobID列保持该作业表中每个作业的唯一标识符。JobTitle列包含作业名称,以便您可以确定哪个作业正在运行。JobInterval列保持连续两个作业之间的时间间隔。它们是大于1/1/1900的日期和时间间隔,在作业成功后,应当将其添加到当前时间中,以计算下一个作业应当运行的时间。例如,JobInterval字段中的值1/2/1901 意味着需要将一年和一天添加到该作业上次运行的时间中。

    DateLastJobRan列包含作业上次运行的日期和时间的datetime值。最后一列 DateNextJobStart包含作业下一次应当运行的时间。尽管该列应当是一个由JobInterval加DateLastJobRan计算结果而得的列,但如果您将该列设置为常规的datetime列,则可更生动地理解应用程序层。

检索和设置作业计时信息

    要通过SQL Server数据库中新的存储过程检索和设置作业计时信息,这些存储过程必须找到该数据库所有需要由该应用程序运行的作业,更新该数据库中单个作业的信息以指示它已经运行,并且设置该作业的下一个作业运行日期。每个作业都在该数据库中具有一个DateNextJobStart列,以指示该作业应当运行的日期和时间。如果当前日期和时间已经超过DateNextJobStart列的日期和时间,则应当在该进程中运行该作业。选择应该运行的作业的存储过程如下所示:

Create PROCEDURE
dbo.Job_SelectJobs_NextJobStartBefore
@DateNextJobRunStartBefore datetime
AS
Select * FROM JOB Where DateNextJobStart < @DateNextJobRunStartBefore

    该存储过程在Job表中选择的作业符合以下条件,其DateNextJobStart值早于(小于)@DateNextJobRunStartBefore DateTime参数的值。要查明应当运行哪些作业,只须通过该存储过程的参数传入当前日期和时间。既然您可以选择需要运行的作业,那么您就可以转而生成该过程以便在作业运行之后更新它们。用单个作业的上一个运行日期和下一个运行日期更新数据库的存储过程如下所示:

Create PROCEDURE dbo.Job_Update_StartEnd_CalcNext
@JobID int,
@DateLastJobRan datetime
AS
Update JOB
SET
DateLastJobRan = @DateLastJobRan,
DateNextJobStart = @DateLastJobRan + JobInterval
Where
JobID = @JobID

    该过程用一个新的DateLastJobRan来更新由@JobID标识的作业,并且通过将JobInterval与传入的@DateLastJobRan相加来计算DateNextJobStart值。该过程只应当在@JobID中引用的作业之后运行,并且应当用等于作业上次运行的日期和时间的@DateLastJobRan参数来调用。

调用作业计时存储过程

    您可以通过添加一个名为JobAccess的新类来扩展数据访问层,以便调用作业计时存储过程。数据访问层中函数的作用是将业务层传递给该层的参数转换为存储过程数据库查询,并且向业务层返回结果。数据访问层的函数中的参数将镜像它们所访问的存储过程的那些参数,因为它们不对这些值执行任何业务逻辑。您将通过Microsoft Data Application Building Block的SQLHelper类访问数据库。该类包含用于简化数据访问代码的功能,从而使您的代码更为简洁,可读性更高。

    要更改数据访问层以运行预定作业,请首先向现有数据访问层中添加JobAccess类,以便保持安排作业所需的函数。接下来,在JobAccess类中创建一个函数,以返回需要通过调用Job_SelectJobs_NextJobStartBefore存储过程运行的作业的DataSet。您还将需要在JobAccess类中创建一个函数以调用Job_Update_StartEnd_CalcNext存储过程,但不返回结果。

    首先,将JobAccess类添加到数据访问层。然后,编辑JobAccess类以添加下列“using”语句:

using System.Data;
using System.Data.SqlClient;
using Microsoft.ApplicationBlocks.Data;

    现在,让我们考察一下如何添加SelectJobsBeforeDate函数以检索需要运行的作业的列表。以下为SQLHelper的ExecuteDataset函数的签名:

public static DataSet
ExecuteDataset(
string connectionString, string spName,
params object[] parameterValues)

    以下为SelectJobsBeforeDate函数,它使用ExecuteDataset来调用Job_Update_StartEnd_CalcNext存储过程,并且返回结果的DataSet:

public DataSet SelectJobsBeforeDate(DateTime beforeDate)
{
    return SqlHelper.ExecuteDataset(
        ConnectionInfo.connectionString,
        "Job_SelectJobs_NextJobStartBefore, myparams);
        new object[]{new SqlParameter("BeforeDate", beforeDate)});
}

    在作业运行之后,您需要执行相应的存储过程以更新有关作业的状态信息。完成该工作的方法UpdateJob将使用SQLHelper类的ExecuteNonQuery方法。以下是它的签名:

public static int ExecuteNonQuery(
    string connectionString, string spName, params object[]
    parameterValues)

    UpdateJob方法可以按以下方式编写:

public void UpdateJob(int JobID, DateTime dateLastJobRan)
{
    string connStr = ConnectionInfo.connectionString;
    string spName = "Job_Update_StartEnd_CalcNext";
    SqlParameter myparam1 = new SqlParameter("JobID", JobID);
    SqlParameter myparam2 = new
        SqlParameter("DateLastJobRan",dateLastJobRan);
    object[] myparams = {myparam1, myparam2};
    SqlHelper.ExecuteNonQuery(connStr, spName, myparams);
}

    JobAccess类中的UpdateJob函数应该镜像传递给它所使用的存储过程的参数。因此,UpdateJob函数具有一个JobID参数和一个dateLastJobRan参数,并且它们的数据类型与Job_Update_StartEnd_CalcNext存储过程中的参数相同。使用JobID和dateLastJobRan参数,您可以创建两个SqlParameters,将它们放到myparams对象数组中,并且使用ExecuteNonQuery函数来执行该存储过程。既然您已经创建了JobAccess类,那么您需要创建最后一个类层,以便将流程层和数据访问层连接起来。

处理预定作业

    最后一个必须加以修改以便处理预定作业的层是业务逻辑层,我将其称为JobLogic。该类将对流程层和数据访问层之间的变量执行基本逻辑。

    首先,使用下列语句向DataAccess层中添加JobLogic类:

using System.Data;
using ScheduledWebService.DataAccess;

    其次,生成JobLogic类的GetAllActiveJobs函数,以查找所有仍然需要在当前时间或之前运行的作业,如下所示:

public DataSet GetAllActiveJobs()
{
    JobAccess ja = new JobAccess();
    return ja.SelectJobsBeforeDate(DateTime.Now);
}

    GetAllActiveJobs函数创建JobAccess类的实例,并且用当前日期的参数值调用它的SelectJobsBeforeDate。GetAllActiveJobs选取当前日期以传递给该函数,因此您可以查明哪些作业被安排在当前时间之前运行。

    最后,创建JobLogic类的UpdateJobDone函数以更新数据库,以便指示所指定的作业刚刚完成,如下所示:

public void UpdateJobDone(int JobID)
{
    JobAccess ja = new JobAccess();
    ja.UpdateJob(JobID, DateTime.Now);
}

    该函数创建JobAccess类的实例并且调用它的UpdateJob方法。它传递JobID参数,然后使用dateLastJobRan参数的当前日期。您需要将当前日期和时间传递给UpdateJob函数,因为它是作业成功完成的时间。

小结

    通过用自动完成的任务扩展ASP.NET应用程序,可以显式计划事件,而不是等待执行代码的请求。您可以利用这一功能执行多种任务——从运行复杂的计算到定期创建报告并将其发送给经理。这样的任务可以同时重用现有的逻辑和ASP.NET层中的对象,并且可以减少开发时间和提高可维护性。您还可以扩展该计划程序启动的作业,而无须更改启动它的Windows服务。

    请注意,对于我在本文中讨论的内容,有许多不同的情况。例如,您可以不创建自定义的Windows服务来充当计划程序,而是使用某种像Windows任务计划程序一样简单的工具,它非常可靠,并且实现了本文中讨论的大多数功能,而无需创建自定义的Windows服务来充当计划程序。总之,.NET Framework已经大大简化了Windows服务的创建,因此即使您先前已经发现它们非常难以使用,您也应当将它们作为一种选择而重新加以考虑。类似地,Web服务是应用程序向其他应用程序公开功能的一种很好的方式,并且将继续在这一方面发挥作用。

作者简介

    Andrew Needleman是Claricode的一名经营合伙人,这是一家位于波士顿附近的咨询公司,专门从事在 .NET中设计和开发N层Web应用程序。他已经培训了数以百计的C#、.NET Framework和Visual Basic .NET领域的开发人员。


posted @ 2007-06-06 11:39  wangyan  阅读(666)  评论(0编辑  收藏  举报