Skip to content

Complete Example

This is a complete, production-ready example demonstrating TickerQ in a real-world scenario. Follow along step-by-step to build a notification system with scheduled emails and cleanup jobs.

Scenario

We'll build a notification system that:

  1. Sends welcome emails 5 minutes after user registration
  2. Sends daily digest emails at 9 AM
  3. Cleans up old notifications daily at midnight
  4. Implements proper error handling and retries

Step-by-Step Walkthrough

Step 1: Project Setup

Scenario

We'll build a notification system that:

  1. Sends welcome emails 5 minutes after user registration
  2. Sends daily digest emails at 9 AM
  3. Cleans up old notifications daily at midnight
  4. Retries failed emails with exponential backoff

Step 1: Install Packages

Install the required NuGet packages:

bash
dotnet add package TickerQ
dotnet add package TickerQ.EntityFrameworkCore
dotnet add package TickerQ.Dashboard
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Why these packages?

  • TickerQ: Core library (required)
  • TickerQ.EntityFrameworkCore: For database persistence
  • TickerQ.Dashboard: For monitoring and management UI
  • Microsoft.EntityFrameworkCore.SqlServer: Database provider

Step 2: Create Domain Models

Define your application models:

csharp
// User.cs
public class User
{
    public Guid Id { get; set; }
    public string Email { get; set; }
    public string Name { get; set; }
    public DateTime CreatedAt { get; set; }
}

// Notification.cs
public class Notification
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string Message { get; set; }
    public DateTime CreatedAt { get; set; }
}

Design Decision: These are your domain models. TickerQ entities are separate infrastructure concerns.

Step 3: Configure Application DbContext (Optional)

If you have application entities that need their own DbContext:

csharp
// AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }
    
    public DbSet<User> Users { get; set; }
    public DbSet<Notification> Notifications { get; set; }
}

Note: TickerQ uses its own built-in TickerQDbContext for job persistence, so your application DbContext remains clean and focused on your domain.

Step 4: Configure Application

csharp
// Program.cs
using TickerQ.DependencyInjection;
using TickerQ.EntityFrameworkCore.DependencyInjection;
using TickerQ.EntityFrameworkCore.DbContextFactory;
using TickerQ.Dashboard.DependencyInjection;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add Entity Framework for application entities (optional)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add TickerQ with built-in TickerQDbContext
builder.Services.AddTickerQ(options =>
{
    // Core configuration
    options.ConfigureScheduler(schedulerOptions =>
    {
        schedulerOptions.MaxConcurrency = 10;
        schedulerOptions.NodeIdentifier = "notification-server";
    });
    
    options.SetExceptionHandler<NotificationExceptionHandler>();
    
    // Entity Framework persistence using built-in TickerQDbContext
    options.AddOperationalStore(efOptions =>
    {
        efOptions.UseTickerQDbContext<TickerQDbContext>(optionsBuilder =>
        {
            optionsBuilder.UseSqlServer(builder.Configuration.GetConnectionString("TickerQConnection"), 
                cfg =>
                {
                    cfg.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5));
                });
        });
        efOptions.SetDbContextPoolSize(34);
    });
    
    // Dashboard
    options.AddDashboard(dashboardOptions =>
    {
        dashboardOptions.SetBasePath("/admin/tickerq");
        dashboardOptions.WithBasicAuth("admin", "secure-password");
    });
});

var app = builder.Build();

app.UseTickerQ();
app.Run();

Why TickerQDbContext? It's lightweight, optimized for TickerQ, and keeps job persistence separate from your application entities. Connection strings are configured directly in TickerQ options.

Step 5: Create Job Functions

Define your job functions with proper error handling:

1. Welcome Email Job

csharp
// NotificationJobs.cs
using TickerQ.Utilities.Base;
using TickerQ.Utilities;

public class NotificationJobs
{
    private readonly IEmailService _emailService;
    private readonly ILogger<NotificationJobs> _logger;
    
    public NotificationJobs(
        IEmailService emailService,
        ILogger<NotificationJobs> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }
    
    [TickerFunction("SendWelcomeEmail")]
    public async Task SendWelcomeEmail(
        TickerFunctionContext context,
        CancellationToken cancellationToken)
    {
        var request = await TickerRequestProvider.GetRequestAsync<WelcomeEmailRequest>(
            context,
            cancellationToken
        );
        
        try
        {
            await _emailService.SendAsync(
                to: request.Email,
                subject: "Welcome!",
                body: $"Hello {request.Name}, welcome to our platform!",
                cancellationToken
            );
            
            _logger.LogInformation("Welcome email sent to {Email}", request.Email);
        }
        catch (SmtpException ex)
        {
            _logger.LogError(ex, "Failed to send welcome email to {Email}", request.Email);
            throw; // Retry on SMTP errors
        }
    }
}

2. Daily Digest Job

Decision: Use cron expression to run daily at 9 AM. This runs automatically without manual scheduling.

csharp
    [TickerFunction("SendDailyDigest", cronExpression: "0 0 9 * * *")]
public async Task SendDailyDigest(
    TickerFunctionContext context,
    CancellationToken cancellationToken)
{
    _logger.LogInformation("Starting daily digest job");
    
    using var scope = context.ServiceScope.ServiceProvider.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    var users = await dbContext.Users.ToListAsync(cancellationToken);
    
    foreach (var user in users)
    {
        try
        {
            var notifications = await dbContext.Notifications
                .Where(n => n.UserId == user.Id 
                    && n.CreatedAt >= DateTime.UtcNow.AddDays(-1))
                .ToListAsync(cancellationToken);
            
            if (notifications.Any())
            {
                await _emailService.SendAsync(
                    to: user.Email,
                    subject: "Daily Digest",
                    body: FormatDigest(notifications),
                    cancellationToken
                );
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send digest to {Email}", user.Email);
            // Continue with other users
        }
    }
    
    _logger.LogInformation("Daily digest job completed");
}

private string FormatDigest(List<Notification> notifications)
{
    var sb = new StringBuilder();
    sb.AppendLine("Your daily digest:");
    foreach (var notification in notifications)
    {
        sb.AppendLine($"- {notification.Message}");
    }
    return sb.ToString();
}

3. Cleanup Job

Decision: Run at midnight (2 AM server time) to avoid peak usage hours.

csharp
    [TickerFunction("CleanupOldNotifications", cronExpression: "0 0 0 * * *")]
public async Task CleanupOldNotifications(
    TickerFunctionContext context,
    CancellationToken cancellationToken)
{
    using var scope = context.ServiceScope.ServiceProvider.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    var cutoffDate = DateTime.UtcNow.AddDays(-30);
    
    var oldNotifications = await dbContext.Notifications
        .Where(n => n.CreatedAt < cutoffDate)
        .ToListAsync(cancellationToken);
    
    dbContext.Notifications.RemoveRange(oldNotifications);
    await dbContext.SaveChangesAsync(cancellationToken);
    
    _logger.LogInformation("Cleaned up {Count} old notifications", oldNotifications.Count);
}

Step 6: Create Request Models

Define typed request models for job data:

csharp
// WelcomeEmailRequest.cs
public class WelcomeEmailRequest
{
    public string Email { get; set; }
    public string Name { get; set; }
    public Guid UserId { get; set; }
}

Why typed requests? Provides compile-time safety and easier debugging.

Step 7: Integrate with Application Services

csharp
// UserService.cs
public class UserService
{
    private readonly AppDbContext _context;
    private readonly ITimeTickerManager<TimeTickerEntity> _timeTickerManager;
    
    public UserService(
        AppDbContext context,
        ITimeTickerManager<TimeTickerEntity> timeTickerManager)
    {
        _context = context;
        _timeTickerManager = timeTickerManager;
    }
    
    public async Task<User> RegisterUserAsync(string email, string name)
    {
        var user = new User
        {
            Id = Guid.NewGuid(),
            Email = email,
            Name = name,
            CreatedAt = DateTime.UtcNow
        };
        
        _context.Users.Add(user);
        await _context.SaveChangesAsync();
        
        // Schedule welcome email
        await _timeTickerManager.AddAsync(new TimeTickerEntity
        {
            Function = "SendWelcomeEmail",
            ExecutionTime = DateTime.UtcNow.AddMinutes(5),
            Request = TickerHelper.CreateTickerRequest(new WelcomeEmailRequest
            {
                Email = email,
                Name = name,
                UserId = user.Id
            }),
            Description = $"Welcome email for {email}",
            Retries = 3,
            RetryIntervals = new[] { 60, 300, 900 } // Exponential backoff
        });
        
        return user;
    }
}

Exception Handler

csharp
// NotificationExceptionHandler.cs
using TickerQ.Utilities.Interfaces;
using TickerQ.Utilities.Enums;

public class NotificationExceptionHandler : ITickerExceptionHandler
{
    private readonly ILogger<NotificationExceptionHandler> _logger;
    private readonly IAlertService _alertService;
    
    public NotificationExceptionHandler(
        ILogger<NotificationExceptionHandler> logger,
        IAlertService alertService)
    {
        _logger = logger;
        _alertService = alertService;
    }
    
    public async Task HandleExceptionAsync(
        Exception exception,
        Guid tickerId,
        TickerType tickerType)
    {
        _logger.LogError(exception,
            "Job {TickerId} ({TickerType}) failed",
            tickerId, tickerType);
        
        // Send alert for critical failures
        if (exception is SmtpException)
        {
            await _alertService.SendAlertAsync(
                "Email service failure",
                exception.ToString()
            );
        }
    }
    
    public async Task HandleCanceledExceptionAsync(
        TaskCanceledException exception,
        Guid tickerId,
        TickerType tickerType)
    {
        _logger.LogWarning(
            "Job {TickerId} ({TickerType}) was cancelled",
            tickerId, tickerType);
    }
}

Controller

csharp
// UserController.cs
[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
    private readonly UserService _userService;
    
    public UserController(UserService userService)
    {
        _userService = userService;
    }
    
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] RegisterRequest request)
    {
        var user = await _userService.RegisterUserAsync(
            request.Email,
            request.Name
        );
        
        return Ok(new { userId = user.Id });
    }
}

Running the Example

1. Create Database

bash
dotnet ef migrations add InitialCreate --context AppDbContext
dotnet ef database update --context AppDbContext

2. Run Application

bash
dotnet run

3. Test Registration

bash
curl -X POST http://localhost:5000/api/users/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","name":"John Doe"}'

4. Monitor Dashboard

Visit http://localhost:5000/admin/tickerq and log in with:

  • Username: admin
  • Password: secure-password

What Happens

  1. User Registration: User registers, and a TimeTicker is scheduled for 5 minutes later
  2. Welcome Email: After 5 minutes, the welcome email job executes
  3. Daily Digest: Every day at 9 AM, daily digest emails are sent
  4. Cleanup: Every day at midnight, old notifications are cleaned up
  5. Retries: If email sending fails, jobs retry with exponential backoff
  6. Monitoring: All jobs are visible in the dashboard

Next Steps

Built by Albert Kunushevci