Files
jdescopingtool/NEW/src/JdeScoping.DataSync/Services/SearchRepository.cs
T
Joseph Doherty 91b516e197 feat: implement WorkProcessor and search execution services
- SearchRepository: Search table operations with Dapper
- SearchExecutionService: Search pipeline with proper cancellation handling
- WorkProcessor: Unified BackgroundService for syncs and searches
- SearchNotificationService: SignalR notifications in Api layer

All 45 new tests pass. Proper shutdown vs timeout distinction
prevents marking searches as error on host shutdown.
2026-01-07 06:18:35 -05:00

137 lines
4.4 KiB
C#

using Dapper;
using JdeScoping.Core.Models.Enums;
using JdeScoping.Core.Models.Search;
using JdeScoping.DataAccess.Interfaces;
using JdeScoping.DataSync.Contracts;
using Microsoft.Extensions.Logging;
namespace JdeScoping.DataSync.Services;
/// <summary>
/// Repository for Search table operations.
/// </summary>
public class SearchRepository : ISearchRepository
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ILogger<SearchRepository> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SearchRepository"/> class.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="logger">The logger instance.</param>
public SearchRepository(
IDbConnectionFactory connectionFactory,
ILogger<SearchRepository> logger)
{
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc/>
public async Task<Search?> GetNextQueuedSearchAsync(CancellationToken ct = default)
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
const string sql = """
SELECT TOP 1
Id, UserName, Name, Status, SubmitDT as SubmitDt,
StartDT as StartDt, EndDT as EndDt, CriteriaJSON as CriteriaJson
FROM dbo.Search
WHERE Status = @Status
ORDER BY SubmitDT ASC
""";
var search = await connection.QueryFirstOrDefaultAsync<Search>(
sql,
new { Status = (int)SearchStatus.Queued },
commandTimeout: 30);
if (search != null)
{
_logger.LogDebug("Found queued search {SearchId}", search.Id);
}
return search;
}
/// <inheritdoc/>
public async Task<int> ResetPartialSearchesAsync(CancellationToken ct = default)
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
const string sql = """
UPDATE dbo.Search
SET Status = @QueuedStatus, StartDT = NULL
WHERE Status = @RunningStatus AND EndDT IS NULL
""";
var count = await connection.ExecuteAsync(
sql,
new
{
QueuedStatus = (int)SearchStatus.Queued,
RunningStatus = (int)SearchStatus.Running
},
commandTimeout: 30);
if (count > 0)
{
_logger.LogWarning("Reset {Count} partial searches to Queued status", count);
}
return count;
}
/// <inheritdoc/>
public async Task StartSearchAsync(int searchId, CancellationToken ct = default)
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
const string sql = """
UPDATE dbo.Search
SET Status = @Status, StartDT = @StartDt
WHERE Id = @SearchId
""";
await connection.ExecuteAsync(
sql,
new
{
SearchId = searchId,
Status = (int)SearchStatus.Running,
StartDt = DateTime.UtcNow
},
commandTimeout: 30);
_logger.LogDebug("Started search {SearchId}", searchId);
}
/// <inheritdoc/>
public async Task CompleteSearchAsync(int searchId, bool success, byte[]? results, CancellationToken ct = default)
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
const string sql = """
UPDATE dbo.Search
SET Status = @Status, EndDT = @EndDt, Results = @Results
WHERE Id = @SearchId
""";
var status = success ? SearchStatus.Ended : SearchStatus.Error;
await connection.ExecuteAsync(
sql,
new
{
SearchId = searchId,
Status = (int)status,
EndDt = DateTime.UtcNow,
Results = results
},
commandTimeout: 30);
_logger.LogDebug("Completed search {SearchId} with status {Status}", searchId, status);
}
}