# Hangfire Background Jobs Standard

> **Scope:** universal
> **Layer:** 2 (on keyword)
> **Keywords:** hangfire, background job, recurring, fire-and-forget, scheduled
> **Load When:** hangfire or background job keywords detected

Background job processing for .NET applications with Hangfire.

---

## Overview

Hangfire provides:
- Fire-and-forget jobs
- Delayed jobs
- Recurring jobs (cron schedules)
- Job continuations
- Built-in dashboard
- Retry logic with exponential backoff

**Stack:** .NET 10

---

## Core Principles

1. **Stateless Jobs**: Job methods should be stateless and idempotent
2. **Minimal Dependencies**: Jobs should not depend on HTTP context or scoped services
3. **Explicit Retries**: Configure retry behavior per job type
4. **Monitor Dashboard**: Use Hangfire Dashboard for job monitoring

---

## Installation & Setup

### Install Packages

```bash
dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore
```

### Program.cs Configuration

```csharp
// Program.cs
using Hangfire;
using Hangfire.SqlServer;

var builder = WebApplication.CreateBuilder(args);

// Add Hangfire services
builder.Services.AddHangfire(configuration => configuration
    .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
    .UseSimpleAssemblyNameTypeSerializer()
    .UseRecommendedSerializerSettings()
    .UseSqlServerStorage(
        builder.Configuration.GetConnectionString("HangfireConnection"),
        new SqlServerStorageOptions
        {
            CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
            SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
            QueuePollInterval = TimeSpan.Zero,
            UseRecommendedIsolationLevel = true,
            DisableGlobalLocks = true
        }));

builder.Services.AddHangfireServer();

var app = builder.Build();

// Hangfire Dashboard (protect in production!)
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
    Authorization = new[] { new HangfireAuthorizationFilter() }
});

app.Run();
```

---

## Job Types

### Fire-and-Forget Jobs

```csharp
// Enqueue immediately
BackgroundJob.Enqueue<IEmailService>(x => x.SendWelcomeEmail(userId));

// Alternative: Static method
BackgroundJob.Enqueue(() => Console.WriteLine("Hello, world!"));
```

### Delayed Jobs

```csharp
// Run after 1 hour
BackgroundJob.Schedule<IReportService>(
    x => x.GenerateMonthlyReport(),
    TimeSpan.FromHours(1));

// Run at specific time
BackgroundJob.Schedule<INotificationService>(
    x => x.SendReminder(userId),
    DateTimeOffset.UtcNow.AddDays(7));
```

### Recurring Jobs

```csharp
// Run daily at 2 AM UTC
RecurringJob.AddOrUpdate<ICleanupService>(
    "cleanup-old-data",
    x => x.CleanupOldRecords(),
    Cron.Daily(2));

// Run every 15 minutes
RecurringJob.AddOrUpdate<ISyncService>(
    "sync-external-data",
    x => x.SyncData(),
    "*/15 * * * *");
```

### Job Continuations

```csharp
var parentJobId = BackgroundJob.Enqueue<IOrderService>(
    x => x.ProcessOrder(orderId));

BackgroundJob.ContinueJobWith<IEmailService>(
    parentJobId,
    x => x.SendOrderConfirmation(orderId));
```

---

## Job Service Pattern

```csharp
// Services/Jobs/IEmailJobService.cs
public interface IEmailJobService
{
    Task SendWelcomeEmailAsync(string userId);
    Task SendPasswordResetAsync(string email);
}

// Services/Jobs/EmailJobService.cs
public class EmailJobService : IEmailJobService
{
    private readonly IDbContextFactory<AppDbContext> _dbFactory;
    private readonly IEmailSender _emailSender;

    public EmailJobService(
        IDbContextFactory<AppDbContext> dbFactory,
        IEmailSender emailSender)
    {
        _dbFactory = dbFactory;
        _emailSender = emailSender;
    }

    public async Task SendWelcomeEmailAsync(string userId)
    {
        // Use IDbContextFactory for background jobs
        await using var db = await _dbFactory.CreateDbContextAsync();

        var user = await db.Users.FindAsync(userId);
        if (user == null) return;

        await _emailSender.SendAsync(
            user.Email,
            "Welcome!",
            $"Hello {user.Name}!");
    }
}

// Register in DI
builder.Services.AddScoped<IEmailJobService, EmailJobService>();
```

---

## Retry & Error Handling

### Custom Retry Attribute

```csharp
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
public async Task ProcessPaymentAsync(string paymentId)
{
    // Job logic with automatic retry on failure
}
```

### Manual Retry Logic

```csharp
public async Task SendEmailWithRetryAsync(string email)
{
    int maxRetries = 3;
    int attempt = 0;

    while (attempt < maxRetries)
    {
        try
        {
            await _emailSender.SendAsync(email, "Subject", "Body");
            return; // Success
        }
        catch (Exception ex)
        {
            attempt++;
            if (attempt >= maxRetries)
            {
                // Log and give up
                _logger.LogError(ex, "Failed to send email after {Attempts} attempts", maxRetries);
                throw;
            }

            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
    }
}
```

---

## Dashboard Authorization

```csharp
// Filters/HangfireAuthorizationFilter.cs
using Hangfire.Dashboard;

public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        var httpContext = context.GetHttpContext();

        // Allow in development
        if (httpContext.Request.Host.Host.Contains("localhost"))
            return true;

        // Require authenticated user with Admin role
        return httpContext.User.Identity?.IsAuthenticated == true
            && httpContext.User.IsInRole("Admin");
    }
}
```

---

## Best Practices

### Avoid Scoped Services

```csharp
// ❌ BAD: Don't inject DbContext directly
public class BadJobService
{
    private readonly AppDbContext _db; // Scoped service!

    public BadJobService(AppDbContext db) => _db = db;
}

// ✅ GOOD: Use IDbContextFactory
public class GoodJobService
{
    private readonly IDbContextFactory<AppDbContext> _dbFactory;

    public GoodJobService(IDbContextFactory<AppDbContext> dbFactory)
        => _dbFactory = dbFactory;

    public async Task ProcessAsync()
    {
        await using var db = await _dbFactory.CreateDbContextAsync();
        // Use db...
    }
}
```

### Idempotent Jobs

```csharp
public async Task ProcessOrderAsync(string orderId)
{
    await using var db = await _dbFactory.CreateDbContextAsync();

    var order = await db.Orders.FindAsync(orderId);
    if (order == null || order.Status == OrderStatus.Processed)
    {
        // Already processed or doesn't exist - skip
        return;
    }

    // Process order...
    order.Status = OrderStatus.Processed;
    await db.SaveChangesAsync();
}
```

---

## Cron Schedule Examples

```csharp
Cron.Minutely()              // Every minute
Cron.Hourly()                // Every hour at minute 0
Cron.Daily()                 // Every day at 00:00 UTC
Cron.Daily(14)               // Every day at 14:00 UTC
Cron.Weekly()                // Every Sunday at 00:00 UTC
Cron.Monthly()               // First day of month at 00:00 UTC
Cron.Yearly()                // January 1st at 00:00 UTC

// Custom cron
"*/5 * * * *"                // Every 5 minutes
"0 */2 * * *"                // Every 2 hours
"0 9-17 * * 1-5"             // 9 AM to 5 PM, Mon-Fri
```

---

## Monitoring & Troubleshooting

### Dashboard Access

Access the dashboard at `/hangfire` (configured in Program.cs).

### Common Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| Jobs not processing | Hangfire server not started | Ensure `AddHangfireServer()` is called |
| DbContext errors | Using scoped DbContext | Use `IDbContextFactory` |
| Jobs timing out | Long-running job | Split into smaller jobs with continuations |
| Memory leaks | Not disposing DbContext | Use `await using` for DbContext |

---

## References

- [Hangfire Documentation](https://docs.hangfire.io/)
- [Hangfire Best Practices](https://docs.hangfire.io/en/latest/best-practices.html)
- [Cron Expression Generator](https://crontab.guru/)

---

*MORPH-SPEC by Polymorphism Tech*
