Example Code Transformations for Lean Migration
This document shows exact code changes required for migrating from Coravel to Windows Task Scheduler, following the lean approach.
1. Job Class Transformation
1.1 BEFORE: IndentEmailScheduler.cs (Coravel Version)
using Coravel.Invocable;
using ErpCrystal_MFG.Models;
using ErpCrystal_MFG.Api.Repositories;
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Controllers;
using System.Text.RegularExpressions;
namespace ErpCrystal_MFG.Api.TaskScheduler;
public class IndentEmailScheduler(IEmailRepository iemailrepository,
IUtilityMethodsRepository iutilitymethodsrepository,
IIndentRepository iindentrepository,
IEnumerable<string> dbnamelist) : IInvocable
{
private readonly IEmailRepository _IEmailRepository = iemailrepository;
private readonly IUtilityMethodsRepository _IUtilityMethodsRepository = iutilitymethodsrepository;
private readonly IIndentRepository _IIndentRepository = iindentrepository;
public async Task Invoke()
{
foreach (var dbname in dbnamelist)
{
string pathDirectoryName = $"\\MFGReports\\Docs\\{dbname}\\Logs";
if (!Directory.Exists(pathDirectoryName))
{
Directory.CreateDirectory(pathDirectoryName);
}
var pathName = $"{pathDirectoryName}\\{dbname}.csv";
var logFilePath = Path.GetFullPath(pathName);
if (!File.Exists(logFilePath))
{
LogMessage(logFilePath, $"Activity,DateTime,Status,Notes");
}
var stdInstructions = await _IUtilityMethodsRepository.GetStdInstructions(dbname, "5");
if (string.IsNullOrEmpty(stdInstructions))
{
LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No Standard Instruction found");
continue;
}
var indentList = await _IIndentRepository.GetScheduledIndents(dbname);
if (indentList.Count == 0)
{
LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No records found");
}
else
{
// ... rest of the business logic
foreach (var data in indentList)
{
// Process each indent
}
}
}
}
private void LogMessage(string path, string message)
{
// Existing logging implementation
using StreamWriter sw = File.AppendText(path);
sw.WriteLine(message);
sw.Close();
}
}1.2 AFTER: IndentEmailJob.cs (Lean Version)
// REMOVED: using Coravel.Invocable;
// KEPT: All other using statements
using ErpCrystal_MFG.Models;
using ErpCrystal_MFG.Api.Repositories;
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Controllers;
using System.Text.RegularExpressions;
namespace ErpCrystal_MFG.Api.Jobs; // CHANGED NAMESPACE
public class IndentEmailJob // REMOVED: : IInvocable
{
private readonly IEmailRepository _IEmailRepository;
private readonly IUtilityMethodsRepository _IUtilityMethodsRepository;
private readonly IIndentRepository _IIndentRepository;
private readonly ITaskSchedulerRepository _ITaskSchedulerRepository; // NEW: To fetch db list
// CHANGED CONSTRUCTOR: Added ITaskSchedulerRepository parameter
public IndentEmailJob(
IEmailRepository iemailrepository,
IUtilityMethodsRepository iutilitymethodsrepository,
IIndentRepository iindentrepository,
ITaskSchedulerRepository itaskschedulerrepository) // NEW parameter
{
_IEmailRepository = iemailrepository;
_IUtilityMethodsRepository = iutilitymethodsrepository;
_IIndentRepository = iindentrepository;
_ITaskSchedulerRepository = itaskschedulerrepository;
}
// NEW METHOD: For single database execution (called by API endpoint with dbName parameter)
public async Task Run(string dbname)
{
// SAME LOGIC from inside foreach loop (almost copy-paste)
string pathDirectoryName = $"\\MFGReports\\Docs\\{dbname}\\Logs";
if (!Directory.Exists(pathDirectoryName))
{
Directory.CreateDirectory(pathDirectoryName);
}
var pathName = $"{pathDirectoryName}\\{dbname}.csv";
var logFilePath = Path.GetFullPath(pathName);
if (!File.Exists(logFilePath))
{
LogMessage(logFilePath, $"Activity,DateTime,Status,Notes");
}
// OPTIONAL: Add Windows Scheduler trigger log
LogMessage(logFilePath, $"Job triggered from Windows Scheduler at {DateTime.Now}");
var stdInstructions = await _IUtilityMethodsRepository.GetStdInstructions(dbname, "5");
if (string.IsNullOrEmpty(stdInstructions))
{
LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No Standard Instruction found");
return; // CHANGED: continue → return for single database
}
var indentList = await _IIndentRepository.GetScheduledIndents(dbname);
if (indentList.Count == 0)
{
LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No records found");
return;
}
// ... rest of the business logic (SAME AS BEFORE)
foreach (var data in indentList)
{
// Process each indent (identical to original)
}
}
// NEW METHOD: For all databases (replaces old Invoke() method)
public async Task RunAll()
{
// Fetch database list dynamically from repository (like original Coravel did)
var dbnamelist = await _ITaskSchedulerRepository.GetDbNameList("001");
foreach (var dbname in dbnamelist)
{
await Run(dbname);
}
}
// SAME LOGGING METHOD (unchanged)
private void LogMessage(string path, string message)
{
using StreamWriter sw = File.AppendText(path);
sw.WriteLine(message);
sw.Close();
}
}2. Program.cs Transformations
2.1 BEFORE: Coravel Configuration in Program.cs
// Line 7: Coravel using statement
using Coravel;
// Lines 18-19: Coravel service registrations
builder.Services.AddScheduler();
builder.Services.AddQueue();
// Line 166: TaskSchedulerInitializer registration
builder.Services.AddScoped<TaskSchedulerInitializer>();
// Lines 210-215: Scheduler initialization
app.Services.UseScheduler(async scheduler =>
{
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<TaskSchedulerInitializer>();
await initializer.InitializeTasks(scheduler);
});2.2 AFTER: Lean Configuration in Program.cs
// REMOVED: using Coravel; (Delete this line)
// Lines 18-19: REMOVE Coravel service registrations
// builder.Services.AddScheduler(); // COMMENT OUT OR DELETE
// builder.Services.AddQueue(); // COMMENT OUT OR DELETE
// Line 166: REMOVE or comment out TaskSchedulerInitializer registration
// builder.Services.AddScoped<TaskSchedulerInitializer>(); // NO LONGER NEEDED
// NEW: Add job service registrations (add after other repository registrations)
builder.Services.AddScoped<IndentEmailJob>();
builder.Services.AddScoped<InvoiceEmailJob>();
builder.Services.AddScoped<SalesOrderEmailJob>();
builder.Services.AddScoped<ArEmailJob>();
builder.Services.AddScoped<VoucherPaymentEmailJob>();
builder.Services.AddScoped<VoucherReceiptEmailJob>();
builder.Services.AddScoped<AIInsightsJob>();
// Lines 210-215: REMOVE ENTIRE UseScheduler BLOCK
// app.Services.UseScheduler(async scheduler =>
// {
// using var scope = app.Services.CreateScope();
// var initializer = scope.ServiceProvider.GetRequiredService<TaskSchedulerInitializer>();
// await initializer.InitializeTasks(scheduler);
// });3. Controller Implementation
3.1 Complete ScheduledJobsController.cs
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Jobs;
using ErpCrystal_MFG.Api.CustomAttributes;
namespace ErpCrystal_MFG.Api.Controllers;
[ApiController]
[Route("api/jobs")]
[ApiKeyAuth] // Existing authentication attribute
public class ScheduledJobsController : ControllerBase
{
private readonly IndentEmailJob _indentEmailJob;
private readonly InvoiceEmailJob _invoiceEmailJob;
private readonly SalesOrderEmailJob _salesOrderEmailJob;
private readonly ArEmailJob _arEmailJob;
private readonly VoucherPaymentEmailJob _voucherPaymentEmailJob;
private readonly VoucherReceiptEmailJob _voucherReceiptEmailJob;
private readonly AIInsightsJob _aiInsightsJob;
public ScheduledJobsController(
IndentEmailJob indentEmailJob,
InvoiceEmailJob invoiceEmailJob,
SalesOrderEmailJob salesOrderEmailJob,
ArEmailJob arEmailJob,
VoucherPaymentEmailJob voucherPaymentEmailJob,
VoucherReceiptEmailJob voucherReceiptEmailJob,
AIInsightsJob aiInsightsJob)
{
_indentEmailJob = indentEmailJob;
_invoiceEmailJob = invoiceEmailJob;
_salesOrderEmailJob = salesOrderEmailJob;
_arEmailJob = arEmailJob;
_voucherPaymentEmailJob = voucherPaymentEmailJob;
_voucherReceiptEmailJob = voucherReceiptEmailJob;
_aiInsightsJob = aiInsightsJob;
}
// Indent Email Job Endpoints
[HttpPost("indent-email")]
public async Task<IActionResult> RunIndentEmails()
{
await _indentEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "Indent email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("indent-email/{dbName}")]
public async Task<IActionResult> RunIndentEmailForDb(string dbName)
{
await _indentEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"Indent email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// Invoice Email Job Endpoints
[HttpPost("invoice-email")]
public async Task<IActionResult> RunInvoiceEmails()
{
await _invoiceEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "Invoice email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("invoice-email/{dbName}")]
public async Task<IActionResult> RunInvoiceEmailForDb(string dbName)
{
await _invoiceEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"Invoice email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// Sales Order Email Job Endpoints
[HttpPost("sales-order-email")]
public async Task<IActionResult> RunSalesOrderEmails()
{
await _salesOrderEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "Sales order email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("sales-order-email/{dbName}")]
public async Task<IActionResult> RunSalesOrderEmailForDb(string dbName)
{
await _salesOrderEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"Sales order email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// AR Email Job Endpoints
[HttpPost("ar-email")]
public async Task<IActionResult> RunArEmails()
{
await _arEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "AR email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("ar-email/{dbName}")]
public async Task<IActionResult> RunArEmailForDb(string dbName)
{
await _arEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"AR email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// Voucher Payment Email Job Endpoints
[HttpPost("voucher-payment-email")]
public async Task<IActionResult> RunVoucherPaymentEmails()
{
await _voucherPaymentEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "Voucher payment email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("voucher-payment-email/{dbName}")]
public async Task<IActionResult> RunVoucherPaymentEmailForDb(string dbName)
{
await _voucherPaymentEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"Voucher payment email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// Voucher Receipt Email Job Endpoints
[HttpPost("voucher-receipt-email")]
public async Task<IActionResult> RunVoucherReceiptEmails()
{
await _voucherReceiptEmailJob.RunAll();
return Ok(new {
Success = true,
Message = "Voucher receipt email job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("voucher-receipt-email/{dbName}")]
public async Task<IActionResult> RunVoucherReceiptEmailForDb(string dbName)
{
await _voucherReceiptEmailJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"Voucher receipt email job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// AI Insights Job Endpoints
[HttpPost("ai-insights")]
public async Task<IActionResult> RunAIInsights()
{
await _aiInsightsJob.RunAll();
return Ok(new {
Success = true,
Message = "AI insights job completed",
Timestamp = DateTime.UtcNow
});
}
[HttpPost("ai-insights/{dbName}")]
public async Task<IActionResult> RunAIInsightsForDb(string dbName)
{
await _aiInsightsJob.Run(dbName);
return Ok(new {
Success = true,
Message = $"AI insights job completed for {dbName}",
Timestamp = DateTime.UtcNow
});
}
// Health Check Endpoint (Optional)
[HttpGet("health")]
public IActionResult HealthCheck()
{
return Ok(new {
Status = "Healthy",
Scheduler = "Windows Task Scheduler",
Timestamp = DateTime.UtcNow
});
}
}4. Minimal API Key Authentication (Already Implemented)
The existing [ApiKeyAuth] attribute should already be implemented. Example of what it might look like:
// Existing in project: ErpCrystal_MFG.Api/CustomAttributes/ApiKeyAuthAttribute.cs
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
// Existing API key validation logic
// This should already be working in your codebase
}
}5. Folder Structure Changes
Current Structure:
ErpCrystal_MFG.Api/
├─ TaskScheduler/
│ ├─ IndentEmailScheduler.cs
│ ├─ InvoiceEmailScheduler.cs
│ ├─ SalesOrderEmailScheduler.cs
│ ├─ ArEmailScheduler.cs
│ ├─ VoucherPaymentEmailScheduler.cs
│ ├─ VoucherReceiptEmailScheduler.cs
│ ├─ AIInsightsScheduler.cs
│ └─ TaskSchedulerInitializer.csNew Structure (After Migration):
ErpCrystal_MFG.Api/
├─ Controllers/
│ ├─ Existing controllers...
│ └─ ScheduledJobsController.cs ← NEW
├─ Jobs/ ← NEW FOLDER
│ ├─ IndentEmailJob.cs ← Renamed/Refactored
│ ├─ InvoiceEmailJob.cs ← Renamed/Refactored
│ ├─ SalesOrderEmailJob.cs ← Renamed/Refactored
│ ├─ ArEmailJob.cs ← Renamed/Refactored
│ ├─ VoucherPaymentEmailJob.cs ← Renamed/Refactored
│ ├─ VoucherReceiptEmailJob.cs ← Renamed/Refactored
│ └─ AIInsightsJob.cs ← Renamed/Refactored
└─ TaskScheduler/ ← DELETE after successful migration6. Migration Notes
Key Differences to Remember:
- Constructor Changes: Job classes need
ITaskSchedulerRepositoryparameter to fetch database lists dynamically - Method Split:
Invoke()becomes two methods:Run(string dbname)andRunAll() - Return vs Continue:
continuein loop becomesreturnin single database method - Namespace: Change from
ErpCrystal_MFG.Api.TaskSchedulertoErpCrystal_MFG.Api.Jobs - Interface Removal: Remove
: IInvocablefrom class definition
Copy-Paste Strategy:
- Copy entire
Invoke()method body - Paste into
Run(string dbname)method - Replace
continuewithreturn - Create
RunAll()method that callsRun()for each database - Update constructor to include
ITaskSchedulerRepository
Testing Strategy:
- Test
Run(dbName)endpoint first with single database - Test
RunAll()endpoint next - Compare logs with Coravel output
- Verify email delivery
7. Common Issues and Solutions
Issue 1: Database list not available in constructor
Solution: Inject ITaskSchedulerRepository and fetch in RunAll() method
Issue 2: Logging paths incorrect
Solution: Keep exact same logging code - it’s path-agnostic
Issue 3: Email attachments not working
Solution: Same business logic - if it worked with Coravel, it will work here
Issue 4: Dependency injection errors
Solution: Ensure all job classes are registered in Program.cs
These example transformations show the minimal changes required to migrate from Coravel to Windows Task Scheduler while keeping 95% of existing business logic unchanged.