استفاده از Hangfire و MediatR به عنوان یک توزیع کننده پیام
از دو کتابخانه محبوب Hangfire و MediatR می توان با هم برای ایجاد یک توزیع کننده پیام خارج از فرآیند(out-of-process) بسیار قدرتمند استفاده کرد.
Hangfire و MediatR Wrapper
اولین کاری که باید انجام دهید این است که یک Wrapper روی MediatR ایجاد کنید. در ابتدا ممکن است این کار کاملاً بیهوده به نظر برسد، اما جلوتر میبیند که هدفی از این کار داریم. Hangfire از نظر نحوه اجرای کارهایی که اغلب اوقات با استفاده از attributes ها تعریف می شوند، توسعه پذیری زیادی دارد. از آنجایی که ما مالک کتابخانه MediatR نیستیم، باید کلاس خود را داشته باشیم که بتوانیم این ویژگی ها را روی آن تعریف کنیم.
در اینجا یک شروع ساده برای Bridge/Wrapper ما است
using System.ComponentModel;
using System.Threading.Tasks;
using MediatR;
namespace Hangfire.MediatR
{
public class MediatorWrapper
{
private readonly IMediator _mediator;
public MediatorWrapper(IMediator mediator)
{
_mediator = mediator;
}
public async Task Send(IRequest command)
{
await _mediator.Send(command);
}
[DisplayName("{0}")]
public async Task Send(string jobName, IRequest command)
{
await _mediator.Send(command);
}
}
}
در مثال بالا، من یک پارامتر اضافه برای ()Send دارم که jobName را به عنوان اولین پارامتر می پذیرد. ویژگی DisplayName توسط Hangfire برای نشان دادن نام کار در داشبورد رابط کاربری استفاده میشود. این یک مثال ساده از این است که چرا ما به این wrapper نیاز داریم.(موارد دیگری هم هست)
MediatR Extensions
قطعه بعدی این پازل ایجاد extension method است تا بتوانید از Hangfire برای ایجاد background job استفاده کنید. در مثال زیر، من متد ()Enqueue ایجاد کردهام که از BackgroundJobClient برای قرار دادن کار با استفاده از Wrapper ما استفاده میکنند.
using MediatR;
namespace Hangfire.MediatR
{
public static class MediatorExtensions
{
public static void Enqueue(this IMediator mediator, string jobName, IRequest request)
{
var client = new BackgroundJobClient();
client.Enqueue<MediatorHangfireBridge>(bridge => bridge.Send(jobName, request));
}
public static void Enqueue(this IMediator mediator,IRequest request)
{
var client = new BackgroundJobClient();
client.Enqueue<MediatorHangfireBridge>(bridge => bridge.Send(request));
}
}
}
سریال سازی Hangfire
در ()BackgroundJobClient.Enqueue بالا، Hangfire از Json.NET برای سریالسازی IRequest که به ()Send ارسال میکنیم , استفاده میکند و در نهایت در فضای ذخیرهسازی قرار میگیرد(که میتونه sql باشه یا redis یا ....). هنگامی که Hangfire آن کار را از فضای ذخیره سازی بازخوانی میکنه تا کار را انجام بده، باید آن را از حالت سریال خارج کند. از آنجا که فقط یک IRequest است، راهی برای تبدیل آن به یک نوع اصلی ندارد.
برای رفع این مشکل، ما باید Hangfire را پیکربندی کنیم تا در صورت serialize/deserialize، نوع کلاس هم اضافه شود.
using Newtonsoft.Json;
namespace Hangfire.MediatR
{
public static class HangfireConfigurationExtensions
{
public static void UseMediatR(this IGlobalConfiguration configuration)
{
var jsonSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
};
configuration.UseSerializerSettings(jsonSettings);
}
}
}
کانفیگ کردن سرویسها
آخرین کاری که باید انجام بدیم , اینه که Hangfire را در ConfigureServices راه اندازی ASP.NET Core یا HostBuilder پیکربندی کنیم.
این فقط یک برنامه کنسوله که صرفاً یک سرور Hangfire است که فقط کارهای Hangfire را پردازش می کند. شما میتونید برنامه وب داشته باشید و قسمت AddHangfire و AddHangfireServer رو در startup اونجا بنویسید
using Hangfire;
using Hangfire.MediatR;
using Microsoft.Extensions.Hosting;
using Sales;
using Shipping;
namespace Worker
{
class Program
{
static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
//......
services.AddHangfire(configuration =>
{
configuration.UseSqlServerStorage("Server=localhost\\SQLEXPRESS;Database=Hangfire;Trusted_Connection=True;");
configuration.UseMediatR();
});
services.AddHangfireServer();
});
}
}
در نوبت گرفتن یک درخواست
برای قرار دادن یک درخواست MediatR به Hangfire، باید متد ()Enqueue را فراخوانی کرد که ما برای IMediator ایجاد کردیم.
using System;
using System.Threading;
using System.Threading.Tasks;
using Hangfire.MediatR;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace Sales
{
public class PlaceOrderController : Controller
{
private readonly IMediator _mediator;
public PlaceOrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost("/sales/orders/{orderId:Guid}")]
public IActionResult Action([FromRoute] Guid orderId)
{
_mediator.Enqueue("Place Order", new PlaceOrder
{
OrderId = orderId
});
return NoContent();
}
}
public class PlaceOrder : IRequest
{
public Guid OrderId { get; set; }
}
public class PlaceOrderHandler : IRequestHandler<PlaceOrder>
{
public Task<Unit> Handle(PlaceOrder request, CancellationToken cancellationToken)
{
// Do your actual work here
throw new NotImplementedException();
}
}
}
در مثال بالا، HTTP Request به Controller یک NoContent 204 را برمی گرداند، حتی یا اینکه در PlaceOrderHandler خود NotImplementedException رو throw کردیم، چون اجرای آن در واقع خارج از زمینه درخواست HTTP انجام می شود. این می تواند در یک thread متفاوت، یک کلن تو یک فرآیند کاملاً متفاوت یا در یک سرور کاملاً متفاوت اجرا بشه.
کارهای بسیار بیشتری می توانید با این کار انجام دهید . کارهایی مانند انتشار رویداد انجام دهید تا هر کنترل کننده رویداد در Hangfire وظیفه خود را داشته باشد.