Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,160 @@
using JdeScoping.DataSync.Configuration;
using JdeScoping.DataSync.Contracts;
using JdeScoping.DataSync.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace JdeScoping.DataSync;
/// <summary>
/// Background service that orchestrates data synchronization from JDE/CMS to SQL Server cache.
/// </summary>
public class DataSyncService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IOptions<DataSyncOptions> _options;
private readonly ILogger<DataSyncService> _logger;
private readonly DataSyncMetrics _metrics;
private DateTime _lastPurgeCheck = DateTime.MinValue;
private readonly TimeSpan _purgeCheckInterval = TimeSpan.FromHours(24);
/// <summary>
/// Initializes a new instance of the <see cref="DataSyncService"/> class.
/// </summary>
public DataSyncService(
IServiceScopeFactory scopeFactory,
IOptions<DataSyncOptions> options,
ILogger<DataSyncService> logger,
DataSyncMetrics metrics)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!_options.Value.Enabled)
{
_logger.LogInformation("DataSyncService is disabled via configuration");
return;
}
_logger.LogInformation(
"DataSyncService starting with CheckInterval={CheckInterval}, MaxDegreeOfParallelism={MaxDegreeOfParallelism}",
_options.Value.CheckInterval,
_options.Value.MaxDegreeOfParallelism);
// Startup: close any interrupted syncs from prior runs
await CloseOpenUpdateEntriesAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Create scope for this sync cycle
await using var scope = _scopeFactory.CreateAsyncScope();
var orchestrator = scope.ServiceProvider.GetRequiredService<ISyncOrchestrator>();
// Check schedules and execute pending syncs
await orchestrator.ExecutePendingSyncsAsync(stoppingToken);
// Periodic purge of old DataUpdate records
await PurgeUpdateEntriesAsync(scope, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful shutdown
_logger.LogInformation("DataSyncService stopping gracefully");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in sync cycle");
_metrics.RecordCycleError();
}
// Wait before next check
try
{
await Task.Delay(_options.Value.CheckInterval, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
}
_logger.LogInformation("DataSyncService stopped");
}
/// <summary>
/// Closes any open update entries from interrupted prior runs.
/// </summary>
private async Task CloseOpenUpdateEntriesAsync(CancellationToken cancellationToken)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IDataUpdateRepository>();
var closedCount = await repository.CloseOpenUpdateEntriesAsync(cancellationToken);
if (closedCount > 0)
{
_logger.LogWarning(
"Closed {Count} interrupted update entries from prior runs",
closedCount);
}
else
{
_logger.LogDebug("No interrupted update entries found");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to close open update entries at startup");
// Continue starting - this is not fatal
}
}
/// <summary>
/// Purges old DataUpdate records periodically.
/// </summary>
private async Task PurgeUpdateEntriesAsync(AsyncServiceScope scope, CancellationToken cancellationToken)
{
if (DateTime.UtcNow - _lastPurgeCheck < _purgeCheckInterval)
{
return;
}
_lastPurgeCheck = DateTime.UtcNow;
try
{
var repository = scope.ServiceProvider.GetRequiredService<IDataUpdateRepository>();
var purgedCount = await repository.PurgeOldEntriesAsync(
_options.Value.PurgeRetentionDays,
cancellationToken);
if (purgedCount > 0)
{
_logger.LogInformation(
"Purged {Count} DataUpdate records older than {Days} days",
purgedCount,
_options.Value.PurgeRetentionDays);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to purge old update entries");
// Continue - this is not fatal
}
}
}