Spensara Spensara Knowledge Base & Documentation Repository

Repository Docs & Knowledge Base

Consolidated Spensara documentation repository (722 Markdown & architecture documents in ../repository)
Built Word Documents (.docx): Spensara Overview (.docx) Capability Assessment (.docx) Brink POS Ingestion Analysis (.docx) The Borg Ingestion Architecture (.docx)

Implementation Specs (157)

Detailed engineering and feature implementation specifications.
implementation-specs/specs/AnomalyDetection/TASK3.2.md 8,771 bytes · Updated Oct 23, 2026 10:44 AM EDT
Raw File

TASK 3.2: Implement EfMonitorSpecRepository

Summary

Implement the IMonitorSpecRepository interface using Entity Framework Core for database access. The implementation lives in the Data project alongside the DbContext and entities it operates on.

Dependencies

  • TASK 1.1 (MonitorSpec entity)
  • TASK 1.3 (Migration applied)
  • TASK 3.1 (IMonitorSpecRepository interface)

Files to Create

1. backend/Kds.Ai.Data/Repositories/EfMonitorSpecRepository.cs

csharp
using Kds.Ai.Data.Entities;
using Kds.Ai.Data.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Kds.Ai.Data.Repositories;

/// <summary>
/// Entity Framework Core implementation of IMonitorSpecRepository
/// </summary>
public sealed class EfMonitorSpecRepository : IMonitorSpecRepository
{
    private readonly KdsAiDbContext _dbContext;
    private readonly ILogger<EfMonitorSpecRepository> _logger;
    
    public EfMonitorSpecRepository(
        KdsAiDbContext dbContext,
        ILogger<EfMonitorSpecRepository> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }
    
    public async Task<MonitorSpec?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .FirstOrDefaultAsync(x => x.Id == id, ct);
    }
    
    public async Task<MonitorSpec?> GetByNameAsync(string name, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .FirstOrDefaultAsync(x => x.Name == name, ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetByStateAsync(
        MonitorState state, 
        CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .Where(x => x.State == state)
            .OrderBy(x => x.Name)
            .ToListAsync(ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetActiveAsync(CancellationToken ct = default)
    {
        return await GetByStateAsync(MonitorState.Active, ct);
    }
    
    public async Task<IReadOnlyList<MonitorSpec>> GetAllAsync(CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .OrderBy(x => x.Name)
            .ToListAsync(ct);
    }
    
    public async Task<MonitorSpec> CreateAsync(MonitorSpec spec, CancellationToken ct = default)
    {
        if (spec.Id == Guid.Empty)
        {
            spec.Id = Guid.NewGuid();
        }
        
        spec.CreatedAt = DateTimeOffset.UtcNow;
        
        _dbContext.MonitorSpecs.Add(spec);
        await _dbContext.SaveChangesAsync(ct);
        
        _logger.LogInformation(
            "Created MonitorSpec {Id} '{Name}' in state {State}",
            spec.Id, spec.Name, spec.State);
        
        return spec;
    }
    
    public async Task<MonitorSpec> UpdateAsync(MonitorSpec spec, CancellationToken ct = default)
    {
        spec.UpdatedAt = DateTimeOffset.UtcNow;
        
        _dbContext.MonitorSpecs.Update(spec);
        await _dbContext.SaveChangesAsync(ct);
        
        _logger.LogInformation("Updated MonitorSpec {Id}", spec.Id);
        
        return spec;
    }
    
    public async Task UpdateLastRunAsync(Guid id, DateTimeOffset lastRunAt, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.LastRunAt, lastRunAt)
                .SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow), ct);
        
        if (affected == 0)
        {
            _logger.LogWarning("MonitorSpec {Id} not found for LastRunAt update", id);
        }
    }
    
    public async Task SetStateAsync(Guid id, MonitorState state, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(x => x.State, state)
                .SetProperty(x => x.UpdatedAt, DateTimeOffset.UtcNow), ct);
        
        if (affected == 0)
        {
            _logger.LogWarning("MonitorSpec {Id} not found for state transition", id);
        }
        else
        {
            _logger.LogInformation("MonitorSpec {Id} transitioned to {State}", id, state);
        }
    }
    
    public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
    {
        var affected = await _dbContext.MonitorSpecs
            .Where(x => x.Id == id)
            .ExecuteDeleteAsync(ct);
        
        if (affected > 0)
        {
            _logger.LogInformation("Deleted MonitorSpec {Id}", id);
            return true;
        }
        
        _logger.LogWarning("MonitorSpec {Id} not found for deletion", id);
        return false;
    }
    
    public async Task<bool> ExistsByNameAsync(string name, CancellationToken ct = default)
    {
        return await _dbContext.MonitorSpecs
            .AnyAsync(x => x.Name == name, ct);
    }
}

Files to Modify

2. backend/Kds.Ai.Data/DependencyInjectionExtensions.cs

Add repository registration to the existing AddDataModule method:

csharp
using Kds.Ai.Data.Repositories;

// Inside AddDataModule, after the AddDbContext call:

// Register repositories
builder.Services.AddScoped<IMonitorSpecRepository, EfMonitorSpecRepository>();

The full method should look like:

csharp
public static IHostApplicationBuilder AddDataModule(this IHostApplicationBuilder builder, string connectionName = "kdsaidb")
{
    builder.Services.AddDbContext<KdsAiDbContext>(options =>
    {
        var connectionString = builder.Configuration.GetConnectionString(connectionName) ?? "Server=localhost;Database=dummy;";
        options.UseMySQL(connectionString);
    });
    
    // Register repositories
    builder.Services.AddScoped<IMonitorSpecRepository, EfMonitorSpecRepository>();
    
    return builder;
}

Implementation Details

Why Kds.Ai.Data?

The EF Core repository implementation belongs in the Data project because:

  • It directly depends on KdsAiDbContext defined in the same project
  • It uses only types from the Data project (entities, enums)
  • No external dependencies beyond what the Data project already has (Microsoft.EntityFrameworkCore, Microsoft.Extensions.Logging)
  • DI registration in AddDataModule ensures the repository is available to any project that calls AddDataModule, without requiring them to know about the implementation details
  • Keeps the AnomalyDetector project focused on domain logic, not data access plumbing

ExecuteUpdateAsync / ExecuteDeleteAsync

  • Uses EF Core 7+ bulk update/delete operations
  • More efficient than loading entity then saving
  • Returns affected row count for existence checking

Timestamps

  • CreatedAt set on create
  • UpdatedAt set on update operations
  • UTC times used consistently

Logging

  • Info level for successful operations
  • Warning level for not-found scenarios

Ordering

  • Results ordered by Name for predictable iteration

Acceptance Criteria

  • [ ] EfMonitorSpecRepository class created in Kds.Ai.Data/Repositories/ (sealed)
  • [ ] Namespace is Kds.Ai.Data.Repositories
  • [ ] All interface methods implemented
  • [ ] Uses MonitorState enum (not bool IsEnabled)
  • [ ] No references to StoreGuid or MonitorType
  • [ ] Uses ExecuteUpdateAsync for partial updates
  • [ ] Uses ExecuteDeleteAsync for bulk delete
  • [ ] Timestamps managed correctly
  • [ ] Logging integrated
  • [ ] Repository registered in AddDataModule in Kds.Ai.Data/DependencyInjectionExtensions.cs
  • [ ] No repository registration needed in AnomalyDetector's DI
  • [ ] Solution builds without errors

Testing Considerations

csharp
// Example usage in tests - resolve from any project that calls AddDataModule
var repo = serviceProvider.GetRequiredService<IMonitorSpecRepository>();

// Create
var spec = await repo.CreateAsync(new MonitorSpec
{
    Name = "Prep Time Monitor - Downtown",
    SystemPrompt = "You are monitoring prep times...",
    State = MonitorState.Active
});

// Query
var active = await repo.GetActiveAsync();
Assert.Contains(active, s => s.Name == "Prep Time Monitor - Downtown");

// State transition
await repo.SetStateAsync(spec.Id, MonitorState.Paused);

// Delete
var deleted = await repo.DeleteAsync(spec.Id);
Assert.True(deleted);

Notes

  • The implementation assumes a scoped KdsAiDbContext from DI (registered by AddDataModule)
  • No explicit transactions - rely on EF Core's default behavior
  • Consider adding concurrency token if needed
  • Consumer projects (e.g., AnomalyDetector) inject IMonitorSpecRepository - they don't need to know it's EF Core behind the scenes