Планирование asp.net core, используя Quartz.net и SignalR

  • Михаил
  • 8 мин. на прочтение
  • 131
  • 23 Feb 2019
  • 23 Feb 2019

В этой статье показано, как запланированные задачи можно реализовать в ASP.NET Core с помощью Quartz.NET , а затем отобразить информацию о задании на странице ASP.NET Core Razor с помощью SignalR. Параллельное и непараллельное задание реализуются с помощью простого триггера, чтобы показать разницу в том, как выполняются задания. Quartz.NET предоставляет множество функций планирования и имеет простой в использовании API для реализации запланированных заданий. Простое веб-приложение ASP.NET Core Razor Page используется для реализации планировщика и обмена сообщениями SignalR . Пакет Quartz Nuget и пакет Quartz.Extensions.Hosting Nuget используются для реализации службы планирования. Пакет Microsoft.AspNetCore.SignalR.Client используется для отправки сообщений всем прослушивающим клиентам веб-сокетов.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
    <PackageReference Include="Quartz" Version="3.5.0" />
    <PackageReference Include="Quartz.Extensions.Hosting" Version="3.5.0" />
  </ItemGroup>
</Project>

Шаблоны .NET 7 больше не используют класс Startup, вся эта логика теперь может быть реализована непосредственно в файле Program.cs без статического основного файла. Логику ConfigurationServices можно реализовать с помощью экземпляра WebApplicationBuilder . Метод AddQuartz используется для добавления служб планирования. Добавляются два задания: одновременное задание и непараллельное задание. Оба задания запускаются простым триггером каждые пять секунд, который работает вечно. Метод AddQuartzHostedService добавляет службу как размещенную. AddSignalR добавляет службы SignalR.

using AspNetCoreQuartz;
using AspNetCoreQuartz.QuartzServices;
using Quartz;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
builder.Services.AddQuartz(q =>
{
    var conconcurrentJobKey = new JobKey("ConconcurrentJob");
    q.AddJob<ConconcurrentJob>(opts => opts.WithIdentity(conconcurrentJobKey));
    q.AddTrigger(opts => opts
        .ForJob(conconcurrentJobKey)
        .WithIdentity("ConconcurrentJob-trigger")
        .WithSimpleSchedule(x => x
            .WithIntervalInSeconds(5)
            .RepeatForever()));
    var nonConconcurrentJobKey = new JobKey("NonConconcurrentJob");
    q.AddJob<NonConconcurrentJob>(opts => opts.WithIdentity(nonConconcurrentJobKey));
    q.AddTrigger(opts => opts
        .ForJob(nonConconcurrentJobKey)
        .WithIdentity("NonConconcurrentJob-trigger")
        .WithSimpleSchedule(x => x
            .WithIntervalInSeconds(5)
            .RepeatForever()));
});
builder.Services.AddQuartzHostedService(
    q => q.WaitForJobsToComplete = true);

Экземпляр WebApplication используется для добавления промежуточного программного обеспечения, такого как метод Startup Configuration. Добавлена ​​конечная точка SignalR JobsHub для отправки текущих сообщений о выполняемых заданиях в пользовательский интерфейс клиентского браузера.

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<JobsHub>("/jobshub");
});
app.MapRazorPages();
app.Run();

ConconcurrentJob реализует интерфейс IJob и регистрирует сообщения до и после задержки. Клиент SignalR используется для отправки всей информации о задании всем прослушивающим клиентам. Был добавлен семисекундный сон для имитации медленной работы. Задания запускаются каждые 5 секунд, поэтому это не должно привести к изменению поведения, поскольку задания могут выполняться параллельно.

using Microsoft.AspNetCore.SignalR;
using Quartz;
namespace AspNetCoreQuartz.QuartzServices
{
    public class ConconcurrentJob : IJob
    {
        private readonly ILogger<ConconcurrentJob> _logger;
        private static int _counter = 0;
        private readonly IHubContext<JobsHub> _hubContext;
        public ConconcurrentJob(ILogger<ConconcurrentJob> logger,
            IHubContext<JobsHub> hubContext)
        {
            _logger = logger;
            _hubContext = hubContext;
        }
        public async Task Execute(IJobExecutionContext context)
        {
            var count = _counter++;
            var beginMessage = $"Conconcurrent Job BEGIN {count} {DateTime.UtcNow}";
            await _hubContext.Clients.All.SendAsync("ConcurrentJobs", beginMessage);
            _logger.LogInformation(beginMessage);
            Thread.Sleep(7000);
            var endMessage = $"Conconcurrent Job END {count} {DateTime.UtcNow}";
            await _hubContext.Clients.All.SendAsync("ConcurrentJobs", endMessage);
            _logger.LogInformation(endMessage);
        }
    }
}

Класс NonConconcurrentJob почти аналогичен предыдущему заданию, за исключением того, что атрибут DisallowConcurrentExecution используется для предотвращения одновременного выполнения задания. Это означает, что хотя триггер установлен на пять секунд, каждое задание должно ждать завершения предыдущего задания.

[DisallowConcurrentExecution]
public class NonConconcurrentJob : IJob
{
    private readonly ILogger<NonConconcurrentJob> _logger;
    private static int _counter = 0;
    private readonly IHubContext<JobsHub> _hubContext;
    public NonConconcurrentJob(ILogger<NonConconcurrentJob> logger,
           IHubContext<JobsHub> hubContext)
    {
        _logger = logger;
        _hubContext = hubContext;
    }
    public async Task Execute(IJobExecutionContext context)
    {
        var count = _counter++;
        var beginMessage = $"NonConconcurrentJob Job BEGIN {count} {DateTime.UtcNow}";
        await _hubContext.Clients.All.SendAsync("NonConcurrentJobs", beginMessage);
        _logger.LogInformation(beginMessage);
        Thread.Sleep(7000);
        var endMessage = $"NonConconcurrentJob Job END {count} {DateTime.UtcNow}";
        await _hubContext.Clients.All.SendAsync("NonConcurrentJobs", endMessage);
        _logger.LogInformation(endMessage);
    }
}

Класс JobsHub реализует концентратор SignalR и определяет методы для отправки сообщений SignalR. Используются два сообщения: одно для сообщений о параллельных заданиях и одно для сообщений о непараллельных заданиях.

public class JobsHub : Hub
{
    public Task SendConcurrentJobsMessage(string message)
    {
        return Clients.All.SendAsync("ConcurrentJobs",  message);
    }
    public Task SendNonConcurrentJobsMessage(string message)
    {
        return Clients.All.SendAsync("NonConcurrentJobs", message);
    }
}

Пакет Microsoft signalr Javascript используется для реализации клиента, который прослушивает сообщения.

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "microsoft-signalr@5.0.11",
      "destination": "wwwroot/lib/microsoft-signalr/"
    }
  ]
}

Представление страницы Index Razor использует файл Javascript SignalR и отображает сообщения путем добавления элементов HTML.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}
<div class="container">
    <div class="row">
        <div class="col-6">
            <ul id="concurrentJobs"></ul>
        </div>
         <div class="col-6">
            <ul id="nonConcurrentJobs"></ul>
        </div>
    </div>
</div>
<script src="~/lib/microsoft-signalr/signalr.js"></script>

Клиент SignalR добавляет два метода для прослушивания сообщений, отправленных заданиями Quartz.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/jobshub")
    .configureLogging(signalR.LogLevel.Information)
    .build();
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};
connection.onclose(async () => {
    await start();
});
start();
connection.on("ConcurrentJobs", function (message) {
    var li = document.createElement("li");
    document.getElementById("concurrentJobs").appendChild(li);
    li.textContent = `${message}`;
});
connection.on("NonConcurrentJobs", function (message) {
    var li = document.createElement("li");
    document.getElementById("nonConcurrentJobs").appendChild(li);
    li.textContent = `${message}`;
});

Когда приложение запущено и размещенная служба Quartz выполняет запланированные задания, одновременные задания запускаются каждые пять секунд по мере необходимости, а непараллельные задания выполняются каждые семь секунд из-за спящего режима потока. Запуск параллельных или непараллельных заданий с использованием одного определения атрибута — действительно мощная функция Quartz.NET. Quartz.NET предоставляет отличную документацию и имеет очень простой API. Используя SignalR, было бы очень легко реализовать хороший пользовательский интерфейс для мониторинга.