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:
- Sends welcome emails 5 minutes after user registration
- Sends daily digest emails at 9 AM
- Cleans up old notifications daily at midnight
- Implements proper error handling and retries
Step-by-Step Walkthrough
Step 1: Project Setup
Scenario
We'll build a notification system that:
- Sends welcome emails 5 minutes after user registration
- Sends daily digest emails at 9 AM
- Cleans up old notifications daily at midnight
- Retries failed emails with exponential backoff
Step 1: Install Packages
Install the required NuGet packages:
dotnet add package TickerQ
dotnet add package TickerQ.EntityFrameworkCore
dotnet add package TickerQ.Dashboard
dotnet add package Microsoft.EntityFrameworkCore.SqlServerWhy these packages?
TickerQ: Core library (required)TickerQ.EntityFrameworkCore: For database persistenceTickerQ.Dashboard: For monitoring and management UIMicrosoft.EntityFrameworkCore.SqlServer: Database provider
Step 2: Create Domain Models
Define your application models:
// 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:
// 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
// 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
// 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.
[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.
[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:
// 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
// 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
// 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
// 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
dotnet ef migrations add InitialCreate --context AppDbContext
dotnet ef database update --context AppDbContext2. Run Application
dotnet run3. Test Registration
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
- User Registration: User registers, and a TimeTicker is scheduled for 5 minutes later
- Welcome Email: After 5 minutes, the welcome email job executes
- Daily Digest: Every day at 9 AM, daily digest emails are sent
- Cleanup: Every day at midnight, old notifications are cleaned up
- Retries: If email sending fails, jobs retry with exponential backoff
- Monitoring: All jobs are visible in the dashboard
