Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions dotnet-client-libraries/QuartzVsHangfire/QuartzVsHangfire.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuartzVsHangfire", "QuartzVsHangfire\QuartzVsHangfire.csproj", "{FC8EF8A5-F261-45EE-89D1-C803C0229725}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{0C10125C-AEBF-4FA6-9CEF-4F4400ADE3C7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FC8EF8A5-F261-45EE-89D1-C803C0229725}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC8EF8A5-F261-45EE-89D1-C803C0229725}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC8EF8A5-F261-45EE-89D1-C803C0229725}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC8EF8A5-F261-45EE-89D1-C803C0229725}.Release|Any CPU.Build.0 = Release|Any CPU
{0C10125C-AEBF-4FA6-9CEF-4F4400ADE3C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C10125C-AEBF-4FA6-9CEF-4F4400ADE3C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C10125C-AEBF-4FA6-9CEF-4F4400ADE3C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C10125C-AEBF-4FA6-9CEF-4F4400ADE3C7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {936474A8-A312-4C71-8420-9D4762C8A024}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Hangfire;
using QuartzVsHangfire.Services;

namespace QuartzVsHangfire.HangfireSample;

// Hangfire is built around a persistent job queue. We hand it work and it
// stores, runs, and retries that work for us. These two calls are the model.
public static class HangfireJobScheduler
{
// Hangfire: enqueue now, retry automatically, watch it in the dashboard
public static string EnqueueWelcomeEmail(int userId) =>
BackgroundJob.Enqueue<IEmailSender>(x => x.SendWelcomeAsync(userId));

public static void ScheduleNightlyReport() =>
RecurringJob.AddOrUpdate<IReportBuilder>("nightly", x => x.RunAsync(), Cron.Daily);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Hangfire;

namespace QuartzVsHangfire.HangfireSample;

// Hangfire ships a queryable monitoring API — the same data the drop-in
// dashboard renders. Wiring the dashboard itself is one line in Startup:
// app.UseHangfireDashboard("/hangfire"); // needs Hangfire.AspNetCore
public static class HangfireMonitoring
{
public static long ScheduledJobCount()
{
var monitoringApi = JobStorage.Current.GetMonitoringApi();

return monitoringApi.GetStatistics().Scheduled;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Hangfire;

namespace QuartzVsHangfire.HangfireSample;

// Hangfire retries failed jobs for us. We do not write a retry loop; we declare
// the policy with an attribute and Hangfire re-runs the job on the schedule below.
public class HangfireRetryPolicy
{
[AutomaticRetry(Attempts = 5, DelaysInSeconds = new[] { 10, 60, 300 })]
public Task SendWebhookAsync(string url)
{
Console.WriteLine($"Posting to {url}. If this throws, Hangfire retries it.");

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Hangfire;
using Microsoft.Extensions.DependencyInjection;

namespace QuartzVsHangfire.HangfireSample;

// One registration block wires the client, the storage, and the worker that
// drains the queue. After this, we inject IBackgroundJobClient and enqueue.
public static class HangfireStartup
{
public static IServiceCollection AddHangfireJobs(this IServiceCollection services)
{
services.AddHangfire(config => config.UseInMemoryStorage());
services.AddHangfireServer();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Hangfire;

namespace QuartzVsHangfire.HangfireSample;

// Hangfire always needs a storage backend: the persisted queue is the product.
// This sample uses the in-memory store to stay self-contained.
public static class HangfireStorageConfig
{
public static JobStorage UseInMemory()
{
GlobalConfiguration.Configuration.UseInMemoryStorage();

// In production we swap this single line for a durable provider, e.g.
// config.UseSqlServerStorage(connectionString);
// config.UsePostgreSqlStorage(connectionString);
// (Redis storage lives in the paid Hangfire Pro package.)
return JobStorage.Current;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Hangfire;
using Hangfire.InMemory;
using Quartz;
using QuartzVsHangfire.HangfireSample;
using QuartzVsHangfire.QuartzSample;

// Hangfire needs storage because the queue is the product. An in-memory store
// keeps this comparison sample self-contained (no SQL Server or Redis required).
GlobalConfiguration.Configuration.UseInMemoryStorage();

var jobId = HangfireJobScheduler.EnqueueWelcomeEmail(userId: 42);
HangfireJobScheduler.ScheduleNightlyReport();

Console.WriteLine($"Hangfire enqueued the welcome email as job '{jobId}' and scheduled the 'nightly' report.");

// Quartz.NET needs no storage to describe a schedule: the trigger is the product.
var trigger = (ICronTrigger)QuartzTriggerScheduler.BuildNightlyTrigger();

Console.WriteLine($"Quartz.NET built trigger '{trigger.Key.Name}' with cron expression '{trigger.CronExpressionString}'.");
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Quartz;

namespace QuartzVsHangfire.QuartzSample;

public class BackgroundJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
var jobDataMap = context.MergedJobDataMap;

var useJobDataMapConsoleOutput = jobDataMap.GetBoolean("UseJobDataMapConsoleOutput");

if (useJobDataMapConsoleOutput)
{
var consoleOutput = jobDataMap.GetString("ConsoleOutput");
await Console.Out.WriteLineAsync(consoleOutput);
}
else
{
await Console.Out.WriteLineAsync("Executing background job without JobDataMap");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Quartz;

namespace QuartzVsHangfire.QuartzSample;

// Quartz.NET has no dashboard. Monitoring is a listener we attach to the
// scheduler. This one counts completed jobs — the hook a custom UI would use.
public class LoggingJobListener : IJobListener
{
public string Name => "logging-job-listener";

public int ExecutedCount { get; private set; }

public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default)
=> Task.CompletedTask;

public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default)
=> Task.CompletedTask;

public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException,
CancellationToken cancellationToken = default)
{
ExecutedCount++;

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Quartz;

namespace QuartzVsHangfire.QuartzSample;

// Quartz.NET has no automatic retry. We opt in by catching the failure and
// throwing a JobExecutionException that asks the scheduler to refire the job.
public class QuartzRetryJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
try
{
await DoWorkAsync(context);
}
catch (Exception ex)
{
throw new JobExecutionException(ex, refireImmediately: true);
}
}

protected virtual Task DoWorkAsync(IJobExecutionContext context) => Task.CompletedTask;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Quartz;

namespace QuartzVsHangfire.QuartzSample;

// Quartz.NET registers the scheduler, its jobs, and their triggers together.
// The job and the trigger are declared side by side, wired by the job key.
public static class QuartzStartup
{
public static IServiceCollection AddQuartzJobs(this IServiceCollection services)
{
services.AddQuartz(configurator =>
{
var reportJob = new JobKey("nightly-report");

configurator.AddJob<ReportJob>(reportJob);
configurator.AddTrigger(trigger => trigger
.ForJob(reportJob)
.WithIdentity("nightly")
.WithCronSchedule("0 0 2 * * ?"));
});

services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Specialized;
using Quartz;
using Quartz.Impl;

namespace QuartzVsHangfire.QuartzSample;

// Quartz.NET runs fine with no database: RAMJobStore is the default. Durable
// schedules are opt-in — we swap the job store type for an ADO.NET store.
public static class QuartzStorageConfig
{
public static Task<IScheduler> CreateInMemorySchedulerAsync()
{
var properties = new NameValueCollection
{
["quartz.jobStore.type"] = "Quartz.Simpl.RAMJobStore, Quartz"
};

var factory = new StdSchedulerFactory(properties);

return factory.GetScheduler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Quartz;

namespace QuartzVsHangfire.QuartzSample;

// Quartz.NET is built around the trigger. The schedule itself is the unit of
// work: we describe when a job fires and Quartz.NET owns the firing.
public static class QuartzTriggerScheduler
{
// Quartz.NET: the trigger is the unit of work
public static ITrigger BuildNightlyTrigger() =>
TriggerBuilder.Create()
.WithIdentity("nightly")
.WithCronSchedule("0 0 2 * * ?")
.Build();

// Quartz.NET has no fire-and-forget queue. The closest equivalent is a
// trigger that starts immediately instead of firing on a schedule.
public static ITrigger BuildImmediateTrigger() =>
TriggerBuilder.Create()
.WithIdentity("welcome-email")
.StartNow()
.Build();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Quartz;
using QuartzVsHangfire.Services;

namespace QuartzVsHangfire.QuartzSample;

// A Quartz.NET job is a class implementing IJob. The scheduler resolves it from
// DI, so constructor-injected services (here IReportBuilder) just work.
public class ReportJob : IJob
{
private readonly IReportBuilder _reportBuilder;

public ReportJob(IReportBuilder reportBuilder) => _reportBuilder = reportBuilder;

public Task Execute(IJobExecutionContext context) => _reportBuilder.RunAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Hangfire.Core" Version="1.8.24"/>
<PackageReference Include="Hangfire.InMemory" Version="1.0.0"/>
<PackageReference Include="Hangfire.NetCore" Version="1.8.24"/>
<PackageReference Include="Quartz" Version="3.19.1"/>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.19.1"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace QuartzVsHangfire.Services;

public class EmailSender : IEmailSender
{
public Task SendWelcomeAsync(int userId)
{
Console.WriteLine($"Sending welcome email to user {userId}.");

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace QuartzVsHangfire.Services;

public interface IEmailSender
{
Task SendWelcomeAsync(int userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace QuartzVsHangfire.Services;

public interface IReportBuilder
{
Task RunAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace QuartzVsHangfire.Services;

public class ReportBuilder : IReportBuilder
{
public Task RunAsync()
{
Console.WriteLine("Building the nightly report.");

return Task.CompletedTask;
}
}
Loading
Loading