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
