Azure Scheduler

Azure Service Bus includes native support for scheduled message delivery using the ScheduledEnqueueTimeUtc property. In V10, Brighter integrates with Azure Service Bus's built-in scheduling capabilities to provide cloud-native scheduling for Azure workloads.

When to Use Azure Service Bus Scheduler

Azure Service Bus Scheduler is recommended when:

  • Running on Azure: Your application is deployed on Azure (VMs, App Service, AKS, Functions, etc.)

  • Using Service Bus: Already using Azure Service Bus for messaging

  • Cloud-Native: Prefer Azure-managed services over self-hosted solutions

  • Simple Scheduling: Need basic scheduling without complex dependencies

  • Azure Ecosystem: Want to leverage Azure services directly

  • Cost-Effective: Want to avoid additional scheduler infrastructure

When NOT to use:

  • Multi-Cloud: Need scheduler to work across cloud providers

  • On-Premises: Application runs outside Azure

  • Reschedule Support: Need to modify scheduled time without cancel/recreate

  • Complex Scheduling: Need advanced features (recurring schedules, job chains, dependencies)

  • Dashboard: Need visual management interface

How Brighter Integrates with Azure Service Bus Scheduler

Brighter uses Azure Service Bus's native ScheduledEnqueueTimeUtc property through the FireAzureScheduler message approach:

Your Code → CommandProcessor.SendAsync(delay, command)

Brighter creates FireAzureScheduler message

Message sent to scheduler topic with ScheduledEnqueueTimeUtc

Azure Service Bus holds message until scheduled time

[Scheduled time reached]

Azure Service Bus delivers FireAzureScheduler message

AzureSchedulerFiredHandler executes

CommandProcessor dispatches original command

Your Handler executes

Why FireAzureScheduler Approach?

Unlike AWS EventBridge Scheduler (which can directly target any SNS/SQS), Azure Service Bus requires a topic/queue to hold the scheduled message. Brighter uses a centralized scheduler topic to handle all scheduled messages, then dispatches them to the appropriate handlers.

Authentication and Credentials

Azure Service Bus Scheduler uses Azure Service Bus client providers for authentication. Brighter supports multiple authentication methods:

using Azure.Identity;
using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider;

var clientProvider = new ServiceBusManagedIdentityClientProvider(
    "myservicebus.servicebus.windows.net"
);

Visual Studio Credentials (Development)

var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(
    "myservicebus.servicebus.windows.net"
);

Connection String (Simple, Less Secure)

var clientProvider = new ServiceBusConnectionStringClientProvider(
    "Endpoint=sb://myservicebus.servicebus.windows.net/;SharedAccessKeyName=..."
);

Default Azure Credentials (Flexible)

var clientProvider = new ServiceBusDefaultAzureCredentialClientProvider(
    "myservicebus.servicebus.windows.net"
);

Best Practice: Use Managed Identity in production, Visual Studio/Default credentials for development.

RBAC Permissions Required

When using Managed Identity or Azure AD authentication, the identity must have these Azure RBAC roles:

  • Azure Service Bus Data Sender: To send scheduled messages

  • Azure Service Bus Data Receiver: To receive FireAzureScheduler messages (Dispatcher)

# Assign permissions using Azure CLI
az role assignment create \
    --assignee <managed-identity-client-id> \
    --role "Azure Service Bus Data Sender" \
    --scope /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.ServiceBus/namespaces/<namespace>

az role assignment create \
    --assignee <managed-identity-client-id> \
    --role "Azure Service Bus Data Receiver" \
    --scope /subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.ServiceBus/namespaces/<namespace>

NuGet Package

To use Azure Service Bus Scheduler, install:

dotnet add package Paramore.Brighter.MessageScheduler.Azure

Package: Paramore.Brighter.MessageScheduler.Azure

Dependencies:

  • Azure.Messaging.ServiceBus - Azure Service Bus SDK

  • Paramore.Brighter.MessagingGateway.AzureServiceBus - Brighter's Azure Service Bus transport

Configuration

Basic Configuration

Configure Azure Service Bus Scheduler with minimal settings:

using Microsoft.Extensions.Hosting;
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter.MessageScheduler.Azure;
using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider;

var builder = WebApplication.CreateBuilder(args);

// Create Azure Service Bus client provider
var clientProvider = new ServiceBusManagedIdentityClientProvider(
    "myservicebus.servicebus.windows.net"
);

// Configure Brighter with Azure Service Bus Scheduler
builder.Services.AddBrighter(options =>
{
    options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new AzureServiceBusSchedulerFactory(
    clientProvider,
    new RoutingKey("brighter-scheduler-topic")  // Scheduler topic name
))
.AutoFromAssemblies();

var app = builder.Build();

Configuration with Dispatcher (FireAzureScheduler Handler)

Configure the Dispatcher to handle FireAzureScheduler messages:

using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection;
using Paramore.Brighter.MessagingGateway.AzureServiceBus;
using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider;

var builder = WebApplication.CreateBuilder(args);

var clientProvider = new ServiceBusManagedIdentityClientProvider(
    "myservicebus.servicebus.windows.net"
);

// Configure producer registry (includes scheduler topic)
var producerRegistry = new AzureServiceBusProducerRegistryFactory(
    clientProvider,
    [
        new AzureServiceBusPublication
        {
            Topic = new RoutingKey("brighter-scheduler-topic"),
            RequestType = typeof(FireAzureScheduler)
        }
    ]
).Create();

// Configure subscription to handle FireAzureScheduler messages
var subscriptions = new[]
{
    new AzureServiceBusSubscription<FireAzureScheduler>(
        new SubscriptionName("scheduler-subscription"),
        new ChannelName("scheduler-channel"),
        new RoutingKey("brighter-scheduler-topic"),
        bufferSize: 10,
        timeOut: TimeSpan.FromSeconds(30),
        messagePumpType: MessagePumpType.Proactor
    )
};

builder.Services.AddServiceActivator(options =>
{
    options.Subscriptions = subscriptions;
})
.UseExternalBus(configure =>
{
    configure.ProducerRegistry = producerRegistry;
})
.UseScheduler(new AzureServiceBusSchedulerFactory(
    clientProvider,
    new RoutingKey("brighter-scheduler-topic")
))
.AutoFromAssemblies();

var app = builder.Build();

Configuration with Custom Sender Options

Customize Azure Service Bus sender behavior:

using Azure.Messaging.ServiceBus;

var schedulerFactory = new AzureServiceBusSchedulerFactory(
    clientProvider,
    new RoutingKey("brighter-scheduler-topic")
)
{
    SenderOptions = new ServiceBusSenderOptions
    {
        // Identifier for the sender
        Identifier = "BrighterScheduler"
    }
};

Configuration with Custom TimeProvider

Use a custom time provider for testing:

var schedulerFactory = new AzureServiceBusSchedulerFactory(
    clientProvider,
    new RoutingKey("brighter-scheduler-topic")
)
{
    TimeProvider = TimeProvider.System  // Or custom for testing
};

Code Examples

Basic Scheduling with Delay

Schedule a command to execute after a delay:

public class OrderService
{
    private readonly IAmACommandProcessor _commandProcessor;

    public async Task CreateOrder(Order order)
    {
        await _repository.SaveAsync(order);

        // Schedule payment processing for 30 minutes later
        var schedulerId = await _commandProcessor.SendAsync(
            TimeSpan.FromMinutes(30),
            new ProcessPaymentCommand { OrderId = order.Id, Amount = order.Total }
        );

        _logger.LogInformation(
            "Scheduled payment processing for order {OrderId}: {SchedulerId}",
            order.Id,
            schedulerId
        );
    }
}

Scheduling with Absolute Time

Schedule for a specific time:

public class ReportService
{
    private readonly IAmACommandProcessor _commandProcessor;

    public async Task ScheduleDailyReport()
    {
        // Schedule for tomorrow at 9 AM UTC
        var tomorrow9AM = DateTimeOffset.UtcNow.Date.AddDays(1).AddHours(9);

        var schedulerId = await _commandProcessor.SendAsync(
            tomorrow9AM,
            new GenerateDailyReportCommand { Date = DateTime.UtcNow.Date }
        );

        _logger.LogInformation("Scheduled daily report: {SchedulerId}", schedulerId);
    }
}

Cancelling a Scheduled Job

Cancel a scheduled task before it executes:

public class OrderService
{
    private readonly IAmACommandProcessor _commandProcessor;
    private readonly IMessageScheduler _scheduler;

    public async Task CancelOrder(Guid orderId)
    {
        var order = await _repository.GetAsync(orderId);

        // Cancel scheduled payment
        if (!string.IsNullOrEmpty(order.PaymentSchedulerId))
        {
            await _scheduler.CancelAsync(order.PaymentSchedulerId);
            _logger.LogInformation(
                "Cancelled scheduled payment for order {OrderId}",
                orderId
            );
        }

        order.Status = OrderStatus.Cancelled;
        await _repository.UpdateAsync(order);
    }
}

Rescheduling (Cancel + Schedule)

Since Azure doesn't support rescheduling, cancel and create a new schedule:

public class OrderService
{
    private readonly IAmACommandProcessor _commandProcessor;
    private readonly IMessageScheduler _scheduler;

    public async Task ReschedulePayment(Guid orderId, TimeSpan newDelay)
    {
        var order = await _repository.GetAsync(orderId);

        // Azure Service Bus doesn't support rescheduling
        // Must cancel existing schedule and create a new one
        if (!string.IsNullOrEmpty(order.PaymentSchedulerId))
        {
            await _scheduler.CancelAsync(order.PaymentSchedulerId);
        }

        // Create new schedule
        var newSchedulerId = await _commandProcessor.SendAsync(
            newDelay,
            new ProcessPaymentCommand { OrderId = order.Id, Amount = order.Total }
        );

        order.PaymentSchedulerId = newSchedulerId;
        await _repository.UpdateAsync(order);

        _logger.LogInformation(
            "Rescheduled payment for order {OrderId}: {NewSchedulerId}",
            orderId,
            newSchedulerId
        );
    }
}

Comparison with Other Schedulers

Feature
Azure Service Bus
AWS Scheduler
Quartz.NET
Hangfire
InMemory

Cloud Native

✅ Azure Only

✅ AWS Only

Managed Service

Persistence

Azure Managed

AWS Managed

Database

Database

None

Dashboard

Azure Portal

AWS Console

Limited

Yes

No

Cancellation

Reschedule

❌ (Cancel+Schedule)

Native Scheduling

✅ Built-in

❌ Separate Service

Cost Model

Service Bus Pricing

Pay-per-use

Infrastructure

Infrastructure

Free

Setup Complexity

Easy

Moderate (IAM)

Moderate

Easy

Minimal

Production Ready

Multi-Cloud

Strong Naming

When to use Azure Service Bus Scheduler:

  • Running on Azure infrastructure

  • Already using Azure Service Bus for messaging

  • Want simplicity (no separate scheduler service)

  • Don't need reschedule support (cancel+schedule is acceptable)

Best Practices

1. Use Managed Identity in Production

// Good - Managed Identity (no credentials in code)
var clientProvider = new ServiceBusManagedIdentityClientProvider(
    "myservicebus.servicebus.windows.net"
);
// Bad - Connection string in code (security risk)
var clientProvider = new ServiceBusConnectionStringClientProvider(
    "Endpoint=sb://...;SharedAccessKeyName=...;SharedAccessKey=..."
);

2. Use Separate Scheduler Topic

// Good - Dedicated scheduler topic
new RoutingKey("brighter-scheduler-topic")
// Bad - Mixing business messages with scheduler messages
new RoutingKey("orders-topic")  // Don't use business topics for scheduling

3. Handle Reschedule as Cancel + Schedule

// Good - Explicit cancel + schedule pattern
if (!string.IsNullOrEmpty(existingSchedulerId))
{
    await _scheduler.CancelAsync(existingSchedulerId);
}
var newSchedulerId = await _commandProcessor.SendAsync(newDelay, command);
// Bad - Trying to reschedule (not supported)
await _scheduler.ReScheduleAsync(existingSchedulerId, newDelay);  // Won't work!

4. Set Appropriate Message TTL

// Good - Configure topic with appropriate message TTL
// In Azure Portal or via ARM template
{
    "defaultMessageTimeToLive": "P14D"  // 14 days
}

5. Monitor with Azure Monitor

// Use Azure Monitor to track:
// - Scheduled message count
// - Delivered message count
// - Dead-letter queue depth
// - Processing errors

// Set up alerts for:
// - Messages in dead-letter queue
// - Processing failures

6. Use Service Bus Premium for Production

// Production recommendation: Use Premium tier
// - Better performance
// - Dedicated resources
// - Virtual network integration
// - Geo-disaster recovery

7. Configure Dead-Letter Queue

// Ensure dead-letter queue is monitored
var subscription = new AzureServiceBusSubscription<FireAzureScheduler>(
    new SubscriptionName("scheduler-subscription"),
    new ChannelName("scheduler-channel"),
    new RoutingKey("brighter-scheduler-topic"),
    deadLetterQueueHandling: true  // Enable DLQ handling
);

8. Test Locally with Azurite

// Good - Test with Azurite (Azure Storage Emulator)
if (environment.IsDevelopment())
{
    var clientProvider = new ServiceBusConnectionStringClientProvider(
        "Endpoint=sb://localhost:5672;..."  // Azurite or local emulator
    );
}

Troubleshooting

Scheduled Messages Not Executing

Symptom: Messages scheduled but never delivered

Possible Causes:

  1. FireAzureScheduler subscription not configured

  2. Incorrect topic name

  3. Message TTL expired before scheduled time

  4. Service Bus connection issues

  5. RBAC permissions missing

Solutions:

// 1. Verify subscription is configured
var subscriptions = new[]
{
    new AzureServiceBusSubscription<FireAzureScheduler>(
        new SubscriptionName("scheduler-subscription"),
        new ChannelName("scheduler-channel"),
        new RoutingKey("brighter-scheduler-topic")
    )
};

// 2. Ensure topic name matches in factory and subscription
new AzureServiceBusSchedulerFactory(clientProvider, new RoutingKey("brighter-scheduler-topic"))

// 3. Check message TTL in Azure Portal
// 4. Verify Service Bus connection
// 5. Check RBAC permissions for Managed Identity

Authentication Failures

Symptom: UnauthorizedAccessException when scheduling

Cause: Missing RBAC permissions or invalid credentials

Solution:

# Verify managed identity has required roles
az role assignment list \
    --assignee <managed-identity-object-id> \
    --scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ServiceBus/namespaces/<ns>

# Assign missing roles
az role assignment create \
    --assignee <managed-identity-client-id> \
    --role "Azure Service Bus Data Sender" \
    --scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ServiceBus/namespaces/<ns>

Reschedule Not Working

Symptom: Attempting to reschedule fails or has no effect

Cause: Azure Service Bus doesn't support rescheduling

Solution:

// Azure doesn't support reschedule - use cancel + schedule pattern
public async Task<string> RescheduleAsync(string oldSchedulerId, TimeSpan newDelay, Command command)
{
    // Cancel existing schedule
    await _scheduler.CancelAsync(oldSchedulerId);

    // Create new schedule
    var newSchedulerId = await _commandProcessor.SendAsync(newDelay, command);

    return newSchedulerId;
}

Messages Going to Dead-Letter Queue

Symptom: Scheduled messages end up in dead-letter queue

Possible Causes:

  • Handler exceptions not properly handled

  • Message processing timeout exceeded

  • Maximum delivery count reached

  • Invalid message format

Solutions:

// 1. Check dead-letter queue for error details
// 2. Increase lock timeout
var subscription = new AzureServiceBusSubscription<FireAzureScheduler>(
    ...
    lockTimeout: 60  // Increase if processing takes longer
);

// 3. Add proper exception handling in handlers
public override async Task<FireAzureScheduler> HandleAsync(
    FireAzureScheduler command,
    CancellationToken cancellationToken = default)
{
    try
    {
        // Your logic
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing scheduled message");
        throw;  // Or handle appropriately
    }
}

Migration from Other Schedulers

From InMemory Scheduler

// Before (Development)
services.AddBrighter(options => { ... })
    .UseScheduler(new InMemorySchedulerFactory())
    .AutoFromAssemblies();

// After (Production on Azure)
services.AddBrighter(options => { ... })
    .UseScheduler(new AzureServiceBusSchedulerFactory(
        clientProvider,
        new RoutingKey("brighter-scheduler-topic")
    ))
    .AutoFromAssemblies();

Additional Setup Required:

  • Configure FireAzureScheduler subscription in Dispatcher

  • Create scheduler topic in Azure Service Bus

  • Configure RBAC permissions

From Quartz or Hangfire to Azure Service Bus

// Before (Quartz)
services.AddBrighter(options => { ... })
    .UseScheduler(provider =>
    {
        var schedulerFactory = provider.GetRequiredService<ISchedulerFactory>();
        return new QuartzSchedulerFactory(
            schedulerFactory.GetScheduler().GetAwaiter().GetResult()
        );
    })
    .AutoFromAssemblies();

// After (Azure Service Bus)
services.AddBrighter(options => { ... })
    .UseScheduler(new AzureServiceBusSchedulerFactory(
        clientProvider,
        new RoutingKey("brighter-scheduler-topic")
    ))
    .AutoFromAssemblies();

Benefits of moving to Azure Service Bus Scheduler:

  • Simpler (no separate scheduler infrastructure)

  • Native Azure integration

  • Reduced operational complexity

  • No database required

Considerations:

  • Must configure FireAzureScheduler subscription

  • No reschedule support (cancel + schedule instead)

  • Requires FireAzureScheduler topic in Service Bus

Summary

Azure Service Bus Scheduler leverages Azure Service Bus's native scheduling capabilities for cloud-native scheduling:

  • Native Integration - Uses Azure Service Bus ScheduledEnqueueTimeUtc

  • Managed Service - No separate scheduler infrastructure

  • Simple Setup - If already using Azure Service Bus

  • Azure Ecosystem - Integrates with Azure monitoring and security

Recommended for production when running on Azure and using Azure Service Bus. Consider Quartz.NET or Hangfire for multi-cloud or on-premises deployments.

Key Decision Factors:

  • ✅ Use Azure Service Bus Scheduler if: Running on Azure, using Service Bus, want simplicity

  • ✅ Use AWS Scheduler if: Running on AWS, need reschedule support

  • ✅ Use Quartz if: Multi-cloud, complex scheduling, need job clustering

  • ✅ Use Hangfire if: Need dashboard, simple setup, OK without strong naming

  • ✅ Use InMemory if: Testing only, not for production

Important Note: Azure Service Bus does NOT support rescheduling. Use cancel + schedule pattern when rescheduling is needed.

Last updated

Was this helpful?