Menu

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

  1. Fire-and-forget jobs
  2. Delayed jobs
  3. Recurring jobs
  4. 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&nbsp;void&nbsp;Configure(IApplicationBuilder&nbsp;app,&nbsp;IWebHostEnvironment&nbsp;env,&nbsp;ILoggerFactory&nbsp;loggerFactory,&nbsp;IServiceProvider&nbsp;serviceProvider)
{
   ...
   app.UseRouting();
   ...
   app.UseEndpoints(endpoints&nbsp;=>
   {
      GlobalConfiguration.Configuration.UseActivator(new&nbsp;ContainerJobActivator(serviceProvider));
&nbsp; &nbsp; &nbsp; 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.

 

 

Leave a comment