Using MediatR with Hangfire to pass requests that can be processed in the background jobs
I have started refactoring one of my old projects, this is not just a pet project. It is a CRM solution, I developed for one of my customers a while back (around 2017). Things were very different then, it was developed with ASP.NET Core RC 1.2. + Angular 4. I have now converted it to .net 5.0 and the front end to Angular 12+. Was not that easy took me like a month to refactor code and upgrade.
Just like any other asp.net core project I have done in the past, I used MediatR, to keep controllers THIN, how it enabled us to write very clean code.
There were a couple of endpoints that were required for email sending, all endpoints are secured with JWT. Each endpoint had a MediatR handler, encapsulating the business logic. I need to move this email sending task out of the HTTP process and run it in the background. Hangfire is a very good solution for all the background tasks.
With Hangfire you can do
- Fire-and-forget jobs
- Delayed jobs
- Recurring jobs
- Continuations
There are more options in the pro version, but I will use the free version as it serves me well, for this purpose.
There is a great video on youtube by Derek Comartin on his CodeOpinion youtube channel. I will use his implementation for my work, I don’t need to reinvent things (yeah kinda lazy too :)).
Below implementation (HangfireConfigurationExtensions, MediatorExtensions, MediatorHangfireBridge) copied from Derek’s repo, Thanks Derek
HangfireConfigurationExtensions
public static class HangfireConfigurationExtensions { public static void UseMediatR(this IGlobalConfiguration configuration) { var jsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; configuration.UseSerializerSettings(jsonSettings); } }
MediatorExtensions
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)); } }
MediatorHangfireBridge
public class MediatorHangfireBridge { private readonly IMediator _mediator; public MediatorHangfireBridge(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); } }
On your Startup.cs file you need to configure hangfire
public void ConfigureServices(IServiceCollection services) { services.AddHangfireServer(); services.AddHangfire(configuration => { configuration.SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(connectionString, new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, UseRecommendedIsolationLevel = true, UsePageLocksOnDequeue = true, DisableGlobalLocks = true, SchemaName = "Jobs" }) .UseMediatR(); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) { ... app.UseRouting(); ... app.UseEndpoints(endpoints => { GlobalConfiguration.Configuration.UseActivator(new ContainerJobActivator(serviceProvider)); endpoints.MapHangfireDashboard("/hangfire", new DashboardOptions {}); }); }
I am planning to use some of the common services that I already use. They are already registered on IServiceProvider, I need the same services in the hangfire. To support dependency injection – you will need to use the hangfire JobActivator to set IServiceProvider.
public class ContainerJobActivator : JobActivator { private IServiceProvider _container; public ContainerJobActivator(IServiceProvider serviceProvider) { _container = serviceProvider; } public override object ActivateJob(Type type) { return _container.GetService(type); } }
Your message object and handler will be like this
public class CoolEmail { public class CampaignCommand : IRequest { public int CampaignId { get; set; } } public class Handler : IRequestHandler<CampaignCommand> { private readonly IDbContext _context; private readonly IEmailService _emailService; public Handler(IDbContext context, IEmailService emailService) { _context = context; _emailService = emailService; } public async Task<Unit> Handle(CampaignCommand message, CancellationToken cancellationToken) { // Logic to send email return Unit.Value; } } }
Now your endpoint will be something like this
[HttpPut("send-a-cool-email/{id}")] public IActionResult SendCoolEmail(int id, CancellationToken cancellationToken) { Mediator.Enqueue("CoolEmailJob", new CampaignCommand { CampaignId = id }); return new OkResult(); }
That is it, now my “cool” email logic will run in the background.