31 июля 2024 г.
Станислав Шолковский
Теги:
Episerver/Optimizely содержит встроенную поддержку ScheduledJobs, которая выполняет свою работу для внутренних заданий EPI и простых пользовательских случаев. Однако, когда приложение растет и нам требуется все больше и больше заданий, она перестает работать из-за специализированных решений.
Что такое Hangfire и зачем его использовать
Hangfire — это фреймворк с открытым исходным кодом, который помогает вам создавать, обрабатывать и управлять фоновыми заданиями, т. е. операциями, которые вы не хотите помещать в конвейер обработки запросов:
- массовые уведомления/рассылки;
- пакетный импорт из xml, csv, json;
- создание архивов;
- запуск веб-хуков;
- удаление пользователей;
- построение различных графиков;
- обработка изображений/видео;
- очистить временные файлы;
- повторяющиеся автоматизированные отчеты;
- ведение базы данных.
Он также бесплатен для коммерческого использования (имеет платные дополнительные функции), а также имеет поддержку политик повторных попыток, различных типов заданий и масштабирования. Что также очень приятно, так это то, что мы можем использовать любой сериализуемый метод, называемый заданием, что тоже очень приятно.
Добавление Hangfire в Episerver/Optimizely 12 на примере проекта Foundation за несколько шагов
Сначала давайте установим пакеты Nuget Hangfire и Hangfire.Console в проект с помощью следующих команд:
dotnet add package Hangfire --version 1.8.14
dotnet add package Hangfire.Console --version 1.4.3
Безопасность превыше всего, поэтому давайте начнем с добавления фильтра авторизации, чтобы гарантировать, что доступ к нему смогут получить только администраторы.
using Hangfire.Annotations;
using Hangfire.Dashboard;
namespace Foundation.Features.Hangfire;
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize([NotNull] DashboardContext context)
{
return EPiServer.Security.PrincipalInfo.CurrentPrincipal.IsInRole("CmsAdmins");
}
}
Чтобы создать удобный пользовательский интерфейс Hangfire, вложенный в пользовательский интерфейс CMS, нам нужно создать для него контроллер.
namespace Foundation.Features.Hangfire;
[Authorize(Roles = "CmsAdmin,WebAdmins,Administrators")]
[Route("[controller]")]
public class HangfireCmsController : Controller
{
[Route("[action]")]
public ActionResult Index()
{
return View();
}
}
Теперь давайте добавим представление для нашего контроллера. Мы будем использовать iframe для Hangfire UI, поэтому сохраним навигацию EPI.
@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation
@{
Layout = string.Empty;
}
lang="en">
Hangfire Dashboard
@ClientResources.RenderResources("ShellCore")
@ClientResources.RenderResources("ShellCoreLightTheme")
html,
body,
.iframe-container {
height: 100%;
}
iframe {
width: 100%;
height: 100%;
}
@Html.CreatePlatformNavigationMenu()
@Html.ApplyPlatformNavigation(additionalClass: "iframe-container")>
После всех проблем с сохранением навигации Epi нам необходимо интегрироваться с ней, поэтому давайте реализуем поставщик меню.
using EPiServer.Authorization;
namespace Foundation.Features.Hangfire;
[MenuProvider]
public class HangfireMenuProvider: IMenuProvider
{
public IEnumerableMenuItem> GetMenuItems()
{
var hangFireMenuItem = new UrlMenuItem("Hangfire", MenuPaths.Global + "/cms" + "/cmsMenuItem",
"/HangfireCms/index")
{
IsAvailable = request => EPiServer.Security.PrincipalInfo.CurrentPrincipal.IsInRole("CmsAdmins"),
AuthorizationPolicy = CmsPolicyNames.CmsAdmin,
SortIndex = SortIndex.First + 25
};
return new MenuItem[]
{
hangFireMenuItem
};
}
}
Было бы обидно иметь только пустой Hangfire UI после завершения этого примера. Давайте добавим простую повторяющуюся работу, которая играет с Hangfire.Console Особенности. Подробнее о заданиях Hangfire можно прочитать в его документации. здесь.
using Hangfire.Console;
using Hangfire.Server;
using System.Threading;
namespace Foundation.Features.Hangfire;
public class ExampleRecurringJob
{
private readonly IContentTypeRepository _contentTypeRepository;
public ExampleRecurringJob(IContentTypeRepository contentTypeRepository)
{
_contentTypeRepository = contentTypeRepository;
}
public void Execute(PerformContext context)
{
context.WriteLine("Hello, world!");
Thread.Sleep(TimeSpan.FromSeconds(1));
context.SetTextColor(ConsoleTextColor.Red);
context.WriteLine("Error! Just joking :)");
Thread.Sleep(TimeSpan.FromSeconds(0.2));
context.ResetTextColor();
var bar = context.WriteProgressBar();
foreach (var contentType in _contentTypeRepository.List().WithProgress(bar))
{
context.WriteLine(contentType.Name);
Thread.Sleep(TimeSpan.FromSeconds(0.3));
}
}
}
Теперь, когда у нас созданы все новые классы, нам нужно добавить конфигурацию Hangfire Startup.cs.
// Add Hangfire services.
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(_configuration.GetConnectionString("EcfSqlConnection"))
.UseConsole());
// Add the processing server as IHostedService
services.AddHangfireServer();
А также эта конфигурация.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapContent();
endpoints.MapHangfireDashboard();
});
var dashboardOptions = new DashboardOptions
{
Authorization = new[]
{
new HangfireAuthorizationFilter()
},
AppPath = null
};
// Order of middlewares is important! Add it after Authentication and Authorization in order to have a user in the context.
app.UseHangfireDashboard("/episerver/backoffice/Plugins/hangfire", dashboardOptions);
RecurringJob.AddOrUpdateExampleRecurringJob>(nameof(ExampleRecurringJob) + "_Id", x => x.Execute(null), Cron.Daily);
Теперь, когда все сделано, мы можем построить и запустить наше приложение. После входа в бэк-офис CMS мы должны сделать что-то вроде этого:

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

Все изменения кода из этого поста можно увидеть в Запрос на пулл-оффер Github Foundation.
