| Este artigo discute | Este artigo usa as seguintes tecnologias: | ||||||||
| C#, .NET, ASP.NET, Windows Download: | ||||||||
Chapéu |
|
Suponha que você tenha escrito um excelente aplicativo n-camadas em ASP.NET e queira estendê-lo para executar tarefas programadas, tais como envio de e-mails para usuários selecionados no banco de dados a cada duas horas ou para analisar regularmente os dados no cache do ASP.NET a fim de monitorar a saúde do aplicativo. Você não quer jogar fora o modelo de objeto de seu aplicativo ASP.NET nem criar muitas dependências entre um agendador separado e o aplicativo ASP.NET, então como pode evitar isso e ainda manter os aplicativos trabalhando juntos?
Nos aplicativos baseados no .NET Framework, os timers são freqüentemente utilizados para realizar atividades em intervalos programados, por isso usar um deles parece ser uma solução apropriada. Você poderia iniciar um timer a partir do handler Application_Start no Global.asax para executar suas tarefas programadas. Infelizmente, essa solução não é ideal em domínios do aplicativo, processos ou reinicializações do sistema porque é necessário solicitar ao aplicativo que este inicie o timer. O ASP.NET é um paradigma de programação passivo que só responde a solicitações de HTTP, por isso um processo ou entrada de usuário deve chamar o código para que ele seja executado.
Uma solução melhor seria usar um Web service para fornecer uma interface com seu aplicativo ASP.NET e para criar um Windows® Service que o chame em intervalos programados. Desse modo, o aplicativo ASP.NET não precisa possuir a lógica de programação e só precisará se preocupar com a execução das tarefas que for capaz de executar. E, como o Web service pode ser executado no mesmo contexto de aplicativo que o restante de seu aplicativo ASP.NET, ele poderá ser executado no mesmo contexto de seu código existente.
Usarei um Windows service para iniciar a chamada do Web service porque os Windows services podem iniciar a si próprios quando o Windows é inicializado. Por isso, mesmo que o servidor seja reiniciado, o aplicativo será capaz de se autoiniciar. Essa capacidade de reinicialização torna o Windows service uma solução mais robusta para a tarefa do que um aplicativo comum baseado em Windows. Essa é também a razão por que os Windows services são usados para tantos processos de background (como IIS).
Neste artigo, demonstrarei como fazer isso durante a criação do menor número possível de dependências entre seu aplicativo de agendamento e o aplicativo ASP.NET. A solução envolve simplificar o aplicativo de programação que inicia o job do ASP.NET. No aplicativo de programação, não será chamada nenhuma lógica que seja específica ao aplicativo ASP.NET, exceto pelo endpoint do Web service que ela chamar. O Windows service usará um arquivo app.config para armazenar tanto o URL do Web service como o intervalo que o Windows service deverá aguardar entre as chamadas para o Web service. Armazenando essas duas configurações no arquivo app.config do Windows service, você poderá alterá-las sem precisar recompilar o Windows service. Caso precise alterar o comportamento do aplicativo quando ele for chamado, você poderá apenas alterar a lógica do aplicativo ASP.NET; no entanto, você não precisará alterar o código do aplicativo de programação. Isso significa que o aplicativo de programação (agendamento) será isolado das mudanças do aplicativo ASP.NET.
Observe que essa solução baseia-se na premissa de que existem algumas tarefas que só deverão ser executadas no contexto de um aplicativo ASP.NET em execução. Se isso não for um requisito para suas tarefas, você deverá considerar fazer uma referência ao assembly da lógica de negócio do aplicativo ASP.NET diretamente de seu Windows service e desviar do processo ASP.NET para acionar as tarefas.
Um aplicativo ASP.NET comum é criado com uma série de camadas independentes que executam funções específicas. Em meu exemplo, tenho classes de acesso de banco de dados, classes de lógica de negócios, classes de fluxos de negócios e páginas ASP.NET que funcionam como o ponto de entrada para essas camadas (observe a Figura 1).

Figura 1 O Plano
As páginas ASP.NET são usadas apenas para exibir e recuperar dados. Elas funcionam como uma interface de/para as classes de fluxo de negócios, que na verdade coordenam todo o trabalho. As classes de fluxo chamam as classes da lógica de negócios na ordem apropriada para concluir uma transação específica, tal como ordenar um widget. Por exemplo, a classe de fluxo poderia primeiro chamar a lógica de negócio para verificar o inventário, depois ordenar o widget e, por fim, reduzir o inventário ao nível apropriado.
As classes da lógica de negócios decidem como chamar as classes de acesso do banco de dados e, se necessário, processam esse resultado para obter o resultado final que você poderá usar para outras operações. Por exemplo, a lógica de negócios seria usada para calcular o preço total, incluindo o imposto para um estado em particular. Primeiro você precisa recuperar a porcentagem de imposto desse estado e dos preços base no banco de dados usando as classes de acesso aos dados e, em seguida, multiplicá-las para encontrar o imposto total de cada item.
As classes de acesso a banco de dados armazenam a lógica que permite a conexão com o banco de dados e o retorno de um resultset em um formato como DataSet, DataTable ou DataReader que possa ser consumido pelas camadas mais altas. Tais classes apenas recuperam os dados do banco de dados e os atualizam de acordo com as informações que recebem; elas não processam o resultado. Por exemplo, elas podem recuperar a porcentagem de imposto de um dado estado, mas não calculam o imposto total do pedido.
O Microsoft® Data Access Application Building Block simplifica as classes de acesso a dados fornecendo maneiras mais fáceis de se comunicar com o banco de dados e com os stored procedures (para o download, consulte Data Access Application Block - http://msdn.microsoft.com/library/en-us/dnbda/html/daab-rm.asp). Por exemplo, você pode fazer uma chamada para o método FillDataSet do objeto SQLHelper para preencher um DataSet a partir da saída de um stored procedure usando uma linha de código. Geralmente, você precisaria escrever o código para criar ao menos o DataAdapter e um objeto de comando, que demandaria pelo menos quatro linhas de código.
O Data Access Application Block se conecta aos stored procedures existentes no banco de dados. Os stored procedures fornecem o código SQL necessário para acessar e modificar os dados no banco de dados.
Um ASP.NET Web service lhe fornecerá uma interface para o aplicativo ASP.NET existente que armazena a lógica da tarefa. Ela funcionará como um intermediário entre ele e o Windows service que coloca o aplicativo ASP.NET em ação. Um Windows service chamará então o aplicativo ASP.NET em intervalos programados. Ao criar um ASP.NET Web service no aplicativo ASP.NET existente, os objetos e a lógica de negócio que anteriormente criados para o aplicativo ASP.NET poderão ser reutilizados nos jobs programados. A Figura 1 mostra os detalhes do fluxo do aplicativo do aplicativo Windows service cliente pelo Web service iniciante que está sendo executado no servidor, durante todo o caminho de execução da tarefa agendada.

Figura 2 Execução de Jobs Programados
Como você pode ver na Figura 3, o processo demandará algumas modificações nas camadas padrão previamente descritas. O Windows service disparar o ASP.NET Web service em um intervalo especificado. O Web service chamará então um método na camada de fluxo do aplicativo Web que determinará quais jobs programados (agendados) deverão ser executados e os executará. Uma vez implementada a solução básica, você usará o arquivo app.config no lado cliente para determinar os intervalos nos quais o Windows service chamará o Web service. Em seguida, você adicionará a funcionalidade exigida pela camada do fluxo de negócios de modo a fazer o loop pelos jobs e executá-los. Seus gurus de n-tier estarão mais interessados na camada do fluxo do que nos fluxos restantes, por isso salvarei a tabela de banco de dados, a stored procedure do banco de dados, o código de acesso dos dados e, por fim, a lógica de negócios.

Figura 3 Inclusões no Aplicativo
Por fim, adicione o código às camadas existentes do aplicativo a partir da parte inferior (o nível da tabela de banco de dados) para o centro (a camada da lógica de negócios) para poder suportar a funcionalidade do job usado pela camada do fluxo.
Para criar o Web service, adicione primeiro o ASP.NET Web service JobRun ao aplicativo ASP.NET dentro da mesma camada que o código ASP.NET existente. Certifique-se de que seu projeto ASP.NET tenha uma referência aos projetos da lógica de negócio, de fluxo e de acesso a dados. Em seguida, para criar o método de Web service RunJob no Web service JobRun, o método de Web service precisará chamar a função da camada de fluxo que executa os jobs apropriados. Isso significa que o método RunJob poderá ser iniciado simplesmente desta maneira:
[WebMethod]
public void RunJob()
{
Flow.JobFlow jf = new Flow.JobFlow();
jf.RunAllActiveJobs();
}
Use a função RunJob para criar uma instância da classe JobFlow (que está na camada do fluxo) e chamar sua função RunAllActiveJobs. O RunAllActiveJobs da função JobFlow faz todo o trabalho real de coordenação dos jobs, enquanto a função RunJob funciona apenas como um ponto de entrada para a seqüência.
Observe que esse código não impede que os jobs sejam executados em mais de uma thread por vez, o que poderia acontecer se o Windows service agendasse tarefas com muita freqüência (mais rápido do que elas pudessem ser executadas) ou se algum outro aplicativo invocasse o ponto de entrada. Se o método não for thread safe e permitir o uso de vários threads ao mesmo tempo, isso poderá causar problemas com os resultados desses jobs. Por exemplo, se o job X enviasse um e-mail para Mary Smith mas ainda não tivesse atualizado o banco de dados quando o job Y o consultou para fazer seus e-mails, Mary poderia receber dois e-mails.
Para sincronizar o acesso à função, usarei a classe Mutex do namespace System.Threading:
private static Mutex mut = new Mutex(false, "JobSchedulerMutex");
O Mutex permite sincronização entre processos e isso impede várias execuções ao mesmo tempo mesmo que haja dois processos de trabalhador ASP.NET envolvidos. Agora, vamos alterar o método RunJob para usar o Mutex e assegurar que não haja outros jobs em execução antes de iniciar os jobs.
Como se pode ver na função RunJob da Listagem 1, você chama a função WaitOne do Mutex para fazer com que essa thread aguarde até que ela seja a única antes da execução. A função ReleaseMutex é então chamada para indicar que você terminou o código que precisa ser executado em apenas uma thread. É claro, o bloqueio aqui pode não ser a solução correta. Você pode escolher retornar imediatamente se houver outra thread executando as tarefas e, neste caso, especificar um timeout breve para o método WaitOne e retornar imediatamente de RunJob se o mutex não puder ser obtido. Coloque todas as ações principais da função em um bloco try-finally para que ReleaseMutex seja chamado mesmo que uma exceção inesperada na função RunAllActiveJobs cause a saída da função RunJob.
Listagem 1 Web service RunJob
[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;
}
Você desejará proteger seu Web service com alguma forma de autenticação e de autorização, possivelmente usando a segurança do Windows, para garantir que ninguém execute o serviço sem autorização apropriada, mas não entrarei em mais detalhes neste artigo. Agora que você criou o Web service de modo a poder chamá-lo de outro aplicativo, vamos criar o Windows service que irá utilizá-lo.
Comece por criar um novo projeto de Windows service em outra instância do Visual Studio® .NET e denomine-o InvokingASPNetService.cs. Certifique-se de que esse serviço será iniciado apropriadamente, adicionando o seguinte método Main:
public static void Main()
{
ServiceBase.Run(new InvokingASPNetService());
}
Agora, adicione as instruções using para os seguintes namespaces:
using System.Configuration; using System.Globalization;
Adicione um instalador para o serviço, clicando com o botão direito na superfície de design do InvokingASPNetService.cs e selecionando Add Installer. Você deve alterar a propriedade serviceInstaller1's StartType criada para Automatic, para que o Windows service seja iniciado junto com o Windows. Defina a propriedade ServiceName de serviceInstaller1 como InvokingASPNetService, para que ela seja denominada apropriadamente em seu Services Manager e, em seguida, altere a propriedade serviceProcessInstaller1 Account para Local Service.
A terceira etapa é criar uma referência da Web para o Web service InvokingASPNetService e, em seguida, denominá-la JobRunWebService. Altere a propriedade JobRunWebService URL Behavior para Dynamic para fazer com que o Visual Studio .NET aumente automaticamente o app.config com o URL de referência da Web. A classe de proxy gerada procurará esse arquivo de configuração para o URL do Web service, permitindo assim que você aponte para o Windows service em um endpoint diferente sem recompilar.
A quarta etapa consiste em criar um método no Windows service para executar o Web service a cada vez que ele for chamado. O método ficará assim:
private void RunCommands()
{
JobRunWebService.JobRunInterval objJob =
new JobRunWebService.JobRunInterval();
objJob.RunJob();
}
Como se pode ver, você declarará o proxy do Web service e o criará como se fosse qualquer outro objeto .NET. Em seguida, chame o método RunJob do Web service para executar os jobs no servidor Web remoto. Observe que nenhuma das etapas é diferente de usar uma classe local mesmo que você esteja usando um Web service.
Quinto: você precisará chamar a função RunCommands do Windows service. Você deve chamar esse método em um intervalo de tempo predefinido, com base na freqüência em que gostaria de executar os jobs no servidor remoto. Use um objeto System.Timers.Timer para garantir que a função RunCommands seja executada em intervalos apropriados. O evento Elapsed do Timer permitirá que você acione qualquer função especificada após o término de cada intervalo. (Observe que o período do intervalo é especificado na propriedade Interval.) Você usará a função acionada para chamar a função RunCommands, de modo a poder automatizar esse recurso. Por padrão, essa classe de timer só aciona um evento na primeira vez que o timer expira, por isso você precisa assegurar que ela se reconfigure repetidamente a cada vez, definindo sua propriedade AutoReset como true.
Você deve declarar isso no nível de serviço, de modo que qualquer função do serviço possa fazer referência a ela:
private Timer timer;
Em seguida, crie uma função para inicializar o timer e defina todos os seus valores relevantes:
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);
}
}
Para permitir que o intervalo de configuração fosse alterado sem recompilar o aplicativo, armazenei o intervalo no arquivo app.config de modo que o método InitializeTimer pusesse acessá-lo por meio de ConfigurationSettings.AppSettings, em vez de por meio de hardcode, conforme mostrado a seguir:
<add key="IntervalMinutes" value="5" />
Certifique-se de que, quando se esgotar, o timer chame a função timer_Elapsed para manipular o evento Elapsed. O método timer_Elapsed é muito simples e chama a função RunCommands recém-criada, conforme mostrado aqui:
private void timer_Elapsed(object source,System.Timers.ElapsedEventArgs e)
{
RunCommands();
}
Por fim, você precisa instalar o Windows service usando o comando installutil. A maneira mais fácil de fazer isso é abrir o prompt de comando do Visual Studio .NET, navegar até o diretório de serviço e executar o utilitário installutil, especificando seu assembly como o parâmetro.
É importante expandir a camada do fluxo para lidar com as necessidades de execução dos jobs programados (presumindo que estes fossem tão diferentes que fosse necessário codificá-los, em vez de apenas incluí-los em parâmetros). Isso envolve coletar todos os jobs do banco de dados onde o próximo tempo de início do banco de dados tenha passado e executá-los individualmente. Dentro da camada do fluxo, você criará uma classe-base denominada Job para fornecer toda a funcionalidade que é comum entre as tarefas. Isso inclui um mecanismo para inicializar e recuperar o JobID, um método comum (RunSingleJob) para executar o job e definir a próxima vez em que ele deverá ser executado no banco de dados após uma execução bem-sucedida, bem como um método de substituição (PerformRunJob) a ser personalizado para cada job.
A camada do fluxo também precisará ter classes específicas à tarefa criadas para cada job que ela executar. Elas herdarão de uma classe-base Job e substituirão a função PerformRunJob da classe Job para personalizar a execução daquele job específico. Você também precisará de uma classe factory (JobFactory) para criar e inicializar a JobID da classe Job correta. A função CreateJob estática criará a tarefa apropriada com base no JobID passado a ela. Por fim, a camada do fluxo precisará ser capaz de determinar quais jobs precisarão ser executados, fazer um loop neles e executá-los. É isso que a classe JobFlow fornecerá através de seu método RunAllActiveJobs.
Primeiro, vamos criar a classe-base Job no projeto da camada de fluxo, que será o pai de cada classe de job individual. O núcleo da classe-base abstrata Job está mostrado na Listagem 2. Ele permite inicializar e recuperar seu JobID, bem como garantir que o banco de dados seja atualizado quando o job é executado com êxito. O JobID não será alterado para um dado job após ter sido criado, por isso você precisa garantir que, após a inicialização, a função definida não alterará o valor. A classe JobFactory que cria cada classe Job definirá seu valor JobID.
Listagem 2 Classe Abstrata 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);
}
A função RunSingleJob determina que o JobID desse job foi inicializado, executa o job (PerformRunJob) e atualiza o banco de dados após as execuções bem-sucedidas com o método RecordJobSuccess. A variável isInitialized é usada para assegurar que cada job tenha seu JobID inicializado antes de executar o job. O método abstrato PerformRunJob é implementado pelas classes Job derivadas e armazena a lógica real para a tarefa.
Após a execução bem-sucedida da implementação do job (método PerformRunJob), a classe-base chamará a função RecordJobSuccess, que usará o método UpdateJobDone da classe JobLogic da camada Business Logic para registrar a hora em que ela foi executada no banco de dados e o horário programado para a próxima execução. Eu criarei a classe JobLogic da camada Business Logic mais adiante.
A classe Job fornece tanto a capacidade de inicializar a variável JobID como de atualizar o banco de dados após o sucesso do próximo tempo de execução. Além disso, você só precisará substituir uma função por um código específico à classe. Isso permite que você crie as classes filhas da classe Job. Para fazer isso, você precisa criar as duas classes que irão executar um dado tipo de job e herdar da classe Job para obter o resto de sua funcionalidade. Crie uma classe JobRunTest e uma classe JobEmailUsers e certifique-se de que cada uma delas herde da classe Job, conforme mostrado a seguir:
public class JobRunTests : Job
Agora, substitua o método PerformRunJob de ambas as classes (usando a classe JobRunTest como amostra):
protected override void PerformRunJob()
{
///Execute aqui a lógica específica ao RunTest
}
Inclua a lógica específica a seu job dentro deste método. O resto do código que executa os jobs e atualiza o próximo tempo de execução no banco de dados é herdado da classe-base Job. Seus jobs irão combinar as chamadas para as classes Business Logic existentes de modo a executar processos complexos. Agora que você tem os jobs de exemplo, vamos ver como criar esses jobs usando o objeto JobFactory.
A classe JobFactory é usada para criar a classe Job filha correspondente para cada JobID. A classe JobFactory utiliza uma variável JobID em sua função CreateJob estática e retorna a subclasse Job apropriada. A Listagem 3 mostra o código na JobFactory.
Listagem 3 Job Factory
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;
}
A função CreateJob pega um currentJobID e o utiliza em uma instrução case para determinar qual classe-filha do Job deve ser retornada. Em seguida, ela inicializa o JobID atual e retorna a classe derivada do Job. Agora que você tem a classe-base Job, suas classes filhas específicas ao job e uma maneira de selecionar a classe a ser criada, pode ver como colocá-las juntas usando a classe JobFlow.
Para criar uma classe denominada JobFlow que irá reunir e executar os jobs apropriados, adicione uma função denominada "RunAllActiveJobs" para fazer o loop em cada job que você precise para executar e chamar suas funções RunSingleJob individuais. Você precisará da função RunAllActiveJobs para capturar uma lista dos jobs que deverão ser executados no banco de dados através da camada de negócio, da camada de acesso aos dados e das stored procedures e, em seguida, executá-los por meio de suas respectivas funções RunSingleJob. O código a seguir mostra como o método RunAllActiveJobs da classe JobFlow alcança essas metas:
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();
}
Basicamente, você iria armazenar os jobs no banco de dados com informações sobre a última vez em que eles foram executados, bem como o intervalo que o código deverá aguardar entre as execuções. Os jobs que precisam ser executados serão então recuperados através da classe JobLogic da camada BusinessLogic com o método GetAllActiveJobs. O ID de cada job ativo será usado para obter um objeto Job, cujo método RunSingleJob poderá ser usado para executar a tarefa, conforme descrito anteriormente.
Determinar quais jobs programados deverão ser executados significa que você precisa armazenar informações básicas sobre eles, tais como o intervalo entre as execuções, a última vez em que eles foram executados e a próxima vez em que eles deverão ser executados. Para fazer isso, crie uma tabela de job em um banco de dados SQL Server (veja a Tabela 1).
Tabela 1 Tabela de Job
A coluna JobID armazena o identificador exclusivo de cada job na tabela de jobs. A coluna JobTitle contém o nome do job para que você possa determinar qual job está sendo executado. A coluna JobInterval armazena o intervalo entre os jobs. Este é o intervalo de data e hora superior a 1/1/1900 que deverá ser adicionado ao horário atual depois que um job for bem-sucedido para calcular quando o próximo job deverá ser executado. Por exemplo, um valor 1/2/1901 no campo JobInterval significa que seriam adicionados um ano e um dia ao horário em que o último job foi executado.
A coluna DateLastJobRan contém um valor datetime para a data e a hora em que o job foi executado pela última vez. A última coluna, DateNextJobStart, contém o próximo horário em que o job deverá ser executado. Embora essa coluna deva ser uma coluna computada, que é igual a JobInterval mais DateLastJobRan, você poderá compreender as camadas do aplicativo de forma mais vívida se a configurar como uma coluna datetime regular.
Para recuperar e definir informações de timing do job através das novas stored procedures no banco de dados SQL Server, as stored procedures deverão encontrar todos os jobs do banco de dados que precisarão ser executados pelo aplicativo, atualizar as informações sobre um único job no banco de dados para indicar que ele foi executado e definir a próxima data de execução desse job. Cada job possui uma coluna DateNextJobStart no banco de dados que indica a data e a hora em que o job deverá ser executado. Se a data e a hora atuais forem anteriores às da coluna DateNextJobStart, o job deverá ser executado no processo. A stored procedure que seleciona os jobs a serem executados é mostrada aqui:
CREATE PROCEDURE dbo.Job_SelectJobs_NextJobStartBefore @DateNextJobRunStartBefore datetime AS SELECT * FROM JOB WHERE DateNextJobStart < @DateNextJobRunStartBefore
Isso seleciona todas as colunas da tabela Job dos jobs que possuem um valor DateNextJobStart que seja anterior (menor que, ou inferior) ao do parâmetro @DateNextJobRunStartBefore DateTime. Para saber quais jobs deverão ser executados, simplesmente informe a data e a hora atuais pelo parâmetro da stored procedure. Agora que você pode selecionar os jobs a serem executados, é hora de criar a procedure que os atualizará após a execução.
A stored procedure que atualiza o banco de dados com a data da última execução e da próxima execução do job é:
CREATE PROCEDURE dbo.Job_Update_StartEnd_CalcNext
@JobID int,
@DateLastJobRan datetime
AS
UPDATE JOB
SET
DateLastJobRan = @DateLastJobRan,
DateNextJobStart = @DateLastJobRan + JobInterval
WHERE
JobID = @JobID
Essa procedure atualiza o job que é identificado pelo @JobID com um novo DateLastJobRan e calcula o valor de DateNextJobStart, adicionando o JobInterval ao @DateLastJobRan que foi fornecido. Essa procedure só poderá ser executada após a execução do job referenciado em @JobID, e deverá ser chamado com um parâmetro @DateLastJobRan igual à data e à hora em que o job foi executado pela última vez.
É possível estender a camada de acesso a dados para chamar as stored procedures de timing do job, adicionando uma nova classe denominada JobAccess. O papel das funções da camada de acesso a dados é traduzir os parâmetros passados a ela pela camada de negócios para uma consulta de banco de dados de stored procedure e retornar o resultado à camada de negócios. Os parâmetros nas funções das camadas de acesso a dados serão espelhados nos parâmetros das stored procedures que eles acessam, já que eles não executam nenhuma lógica de negócios nos valores.
Você acessará o banco de dados através da classe SQLHelper do Data Application Building Block. Essa classe contém a funcionalidade que simplifica o código de acesso aos dados, tornando seu código mais conciso e legível.
Para alterar a camada de acesso a dados para executar os jobs programados, adicione primeiro a classe JobAccess à camada de dados de acesso existente para armazenar as funções necessárias para agendar os jobs. Em seguida, crie uma função na classe JobAccess que retorne um DataSet dos jobs que precisam ser executados através da chamada a stored procedure Job_SelectJobs_NextJobStartBefore. Você também precisará criar uma função na classe JobAccess para chamar a stored procedure Job_Update_StartEnd_CalcNext sem retornar um resultado.
Primeiramente, adicione a classe JobAccess à camada de acesso a dados. Em seguida, edite a classe JobAccess para adicionar as seguintes instruções "using":
using System.Data; using System.Data.SqlClient; using Microsoft.ApplicationBlocks.Data;
Vamos analisar agora como adicionar a função SelectJobsBeforeDate, que recupera a lista de jobs que precisam ser executados. Veja aqui a assinatura da função ExecuteDataset da SQLHelper:
public static DataSet
ExecuteDataset(
string connectionString, string spName,
params object[] parameterValues)
Veja a seguir a função SelectJobsBeforeDate, que utiliza ExecuteDataset para chamar a stored procedure Job_Update_StartEnd_CalcNext, retornando um DataSet dos resultados:
public DataSet SelectJobsBeforeDate(DateTime beforeDate)
{
return SqlHelper.ExecuteDataset(
ConnectionInfo.connectionString,
"Job_SelectJobs_NextJobStartBefore, myparams);
new object[]{new SqlParameter("BeforeDate", beforeDate)});
}
Uma vez executados os jobs, você precisará executar a stored procedure que atualiza as informações de status sobre os jobs. O método que consegue isso, UpdateJob, usará o método ExecuteNonQuery da classe SQLHelper. Veja a assinatura:
public static int ExecuteNonQuery(
string connectionString, string spName, params object[]
parameterValues)
O método UpdateJob pode ser escrito da seguinte maneira:
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);
}
A função UpdateJob na classe JobAccess deve espelhar os parâmetros que são passados a stored procedure que ela utiliza. Desse modo, a função UpdateJob possui um parâmetro jobID e um parâmetro dateLastJobRan com os mesmos datatypes daqueles na stored procedure Job_Update_StartEnd_CalcNext. Usando os parâmetros jobID e dateLastJobRan, você pode criar os dois SqlParameters, colocá-los no array do objeto myparams e usar a função ExecuteNonQuery para executar a stored procedure. Agora que você criou a classe JobAccess, precisa criar a camada final de classes para preencher a distância entre a camada do fluxo e a camada de acesso aos dados.
A camada final que precisa ser modificada para trabalhar com jobs programados é a camada Business Logic, que chamarei de JobLogic. Essa classe executará a lógica básica nas variáveis entre a camada de fluxo e a camada de acesso aos dados.
Primeiramente, adicione a classe JobLogic à camada DataAccess, utilizando as seguintes instruções:
using System.Data; using ScheduledWebService.DataAccess;
Depois, crie a função GetAllActiveJobs da classe JobLogic para localizar todos os jobs que ainda precisam ser executados antes ou durante o momento atual, conforme mostrado aqui:
public DataSet GetAllActiveJobs()
{
JobAccess ja = new JobAccess();
return ja.SelectJobsBeforeDate(DateTime.Now);
}
A função GetAllActiveJobs cria uma instância da classe JobAccess e chama seu SelectJobsBeforeDate com o valor de parâmetro da data atual. O GetAllActiveJobs seleciona a data atual para passar essa função, por isso você poderá descobrir quais jobs foram programados para serem executados antes da hora atual.
Por fim, crie a função UpdateJobDone da classe JobLogic para atualizar o banco de dados de modo a indicar que o job especificado foi recém-concluído, conforme mostrado aqui:
public void UpdateJobDone(int jobID)
{
JobAccess ja = new JobAccess();
ja.UpdateJob(jobID, DateTime.Now);
}
Essa função cria uma instância da classe JobAccess e chama seu método UpdateJob. Ela passa adiante o parâmetro jobID e, em seguida, usa a data atual do parâmetro dateLastJobRan. Você passa a data e a hora atuais para a função UpdateJob porque é o momento em que o job foi concluído com êxito.
Estender seu aplicativo ASP.NET com tarefas automatizadas permite que você programe explicitamente os eventos, em vez de aguardar até que uma solicitação execute o código. Você pode utilizar esse avanço para executar diversas tarefas, desde a execução de cálculos complexos até a criação e o envio de relatórios para executivos em uma agenda regular. Tais tarefas podem reutilizar tanto a lógica existente como os objetos em suas camadas ASP.NET, reduzindo o tempo de desenvolvimento e aumentando a capacidade de manutenção. Você também pode expandir os jobs que esse programador inicia sem alterar o Windows service que o inicializa.
Observe que há muitas variações para o que foi discutido neste artigo. Por exemplo, em vez de criar um Windows service personalizado para atuar como programador, você poderia usar algo mais objetivo, como o Windows Task Scheduler, que é muito robusto e implementa a maior parte dos recursos discutidos aqui. A criação de Windows services foi amplamente simplificada pelo .NET Framework, de modo que você deveria reconsiderá-los como uma opção, mesmo que anteriormente os tenha achado muito difícil de usar. Da mesma forma, os Web services são uma excelente maneira de os aplicativos exporem funcionalidade a outros aplicativos e continuarão a ser valiosos nessa função.
Andrew Needleman (andrew@claricode.com) é sócio-gerente da Claricode, empresa de consultoria sediada perto de Boston e especializada em arquitetura e desenvolvimento de aplicativos Web n-tier em .NET. Andrew já treinou centenas de desenvolvedores em C#, no.NET Framework e no Visual Basic .NET.