Files
jdescopingtool/openspec/changes/archive/2026-01-01-setup-solution-foundation/design.md
T
Joseph Doherty 26ff8d9b4f 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.
2026-01-02 07:43:29 -05:00

8.5 KiB

Solution Foundation Design

Overview

This document describes the infrastructure architecture for the .NET 10 solution, including project structure, dependency injection patterns, and configuration management.

Project Structure

NEW/src/
├── JdeScoping.Core/           # Domain models, interfaces, shared logic
│   ├── Models/                # Entity classes (Search, WorkOrder, Lot, etc.)
│   ├── Interfaces/            # Service contracts (ISearchRepository, etc.)
│   ├── Options/               # Configuration binding classes
│   └── Extensions/            # Service registration extension methods
├── JdeScoping.Host/           # ASP.NET Core host (Web API + BackgroundServices)
│   ├── Program.cs             # Application entry point, DI configuration
│   ├── appsettings.json       # Production configuration
│   ├── appsettings.Development.json  # Development overrides
│   └── Controllers/           # API endpoints
├── JdeScoping.Client/         # Blazor WebAssembly UI
│   └── (deferred to UI phase)
└── JdeScoping.Database/       # DbUp migrations (already exists)
    ├── Scripts/               # Migration SQL files
    └── DatabaseMigrator.cs    # DbUp configuration

DI Registration Pattern

Extension Method Convention

Each module provides an extension method on IServiceCollection:

public static class DataAccessServiceExtensions
{
    public static IServiceCollection AddDataAccess(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Bind configuration
        services.Configure<DataAccessOptions>(
            configuration.GetSection(DataAccessOptions.SectionName));

        // Register services
        services.AddScoped<ILotFinderRepository, LotFinderRepository>();
        services.AddScoped<IJdeRepository, JdeRepository>();
        services.AddScoped<ICmsRepository, CmsRepository>();

        return services;
    }
}

Module Registration Order

Extensions are called in dependency order in Program.cs:

builder.Services
    .AddDataAccess(builder.Configuration)      // 1. Database access
    .AddDataSync(builder.Configuration)        // 2. Cache synchronization
    .AddSearchProcessing(builder.Configuration) // 3. Search execution
    .AddExcelExport(builder.Configuration)     // 4. Result export
    .AddAuth(builder.Configuration);           // 5. Authentication

Lifetime Guidelines

Service Type Lifetime Rationale
Repository Scoped Database connection per request
DbContext (if used) Scoped EF Core default
Options classes Singleton Cached configuration
HttpClient Singleton Connection pooling
BackgroundService Singleton Long-running workers
Processors Transient Stateless operations

Configuration Sections

appsettings.json Structure

{
  "ConnectionStrings": {
    "LotFinder": "Server=...;Database=LotFinder;...",
    "JDE": "Data Source=...;User ID=...;Password=...",
    "CMS": "Data Source=...;Port=...;Database=..."
  },
  "DataAccess": {
    "CommandTimeoutSeconds": 120,
    "EnableDetailedLogging": false
  },
  "DataSync": {
    "MassRefreshCronSchedule": "0 0 6 * * SAT",
    "DailyRefreshCronSchedule": "0 0 4 * * *",
    "HourlyRefreshCronSchedule": "0 0 * * * *",
    "MaxConcurrentUpdates": 4
  },
  "Auth": {
    "LdapUrl": "LDAP://directory.company.com",
    "LdapGroup": "CN=LotFinderUsers,OU=Groups,DC=company,DC=com",
    "CookieExpirationMinutes": 480
  },
  "ExcelExport": {
    "TempDirectory": "/tmp/lotfinder",
    "MaxRowsPerSheet": 1048576,
    "DefaultDateFormat": "yyyy-MM-dd HH:mm:ss"
  },
  "SearchProcessing": {
    "PollingIntervalSeconds": 5,
    "MaxConcurrentSearches": 2,
    "SearchTimeoutMinutes": 30
  }
}

appsettings.Development.json Overrides

{
  "ConnectionStrings": {
    "LotFinder": "Server=localhost,1434;Database=LotFinder;User Id=scopingapp;Password=...;TrustServerCertificate=True"
  },
  "DataAccess": {
    "EnableDetailedLogging": true
  },
  "DataSync": {
    "MassRefreshCronSchedule": "",
    "DailyRefreshCronSchedule": "",
    "HourlyRefreshCronSchedule": ""
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Options Classes

Naming Convention

  • Class name: {Module}Options
  • Section name: Same as class name without "Options" suffix
  • Static constant: SectionName for configuration binding

DataAccessOptions

public class DataAccessOptions
{
    public const string SectionName = "DataAccess";

    public int CommandTimeoutSeconds { get; set; } = 120;
    public bool EnableDetailedLogging { get; set; } = false;
}

DataSyncOptions

public class DataSyncOptions
{
    public const string SectionName = "DataSync";

    public string MassRefreshCronSchedule { get; set; } = "0 0 6 * * SAT";
    public string DailyRefreshCronSchedule { get; set; } = "0 0 4 * * *";
    public string HourlyRefreshCronSchedule { get; set; } = "0 0 * * * *";
    public int MaxConcurrentUpdates { get; set; } = 4;
}

AuthOptions

public class AuthOptions
{
    public const string SectionName = "Auth";

    public string LdapUrl { get; set; } = string.Empty;
    public string LdapGroup { get; set; } = string.Empty;
    public int CookieExpirationMinutes { get; set; } = 480;
}

ExcelExportOptions

public class ExcelExportOptions
{
    public const string SectionName = "ExcelExport";

    public string TempDirectory { get; set; } = "/tmp/lotfinder";
    public int MaxRowsPerSheet { get; set; } = 1048576;
    public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
}

SearchProcessingOptions

public class SearchProcessingOptions
{
    public const string SectionName = "SearchProcessing";

    public int PollingIntervalSeconds { get; set; } = 5;
    public int MaxConcurrentSearches { get; set; } = 2;
    public int SearchTimeoutMinutes { get; set; } = 30;
}

NuGet Package Dependencies

JdeScoping.Core

<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />

JdeScoping.Host

<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="EPPlus" Version="7.0.0" />
<PackageReference Include="DbUp-SqlServer" Version="5.0.37" />
<PackageReference Include="Quartz" Version="3.8.0" />

Program.cs Structure

var builder = WebApplication.CreateBuilder(args);

// Database migrations
DatabaseMigrator.Migrate(builder.Configuration.GetConnectionString("LotFinder")!);

// Module registration
builder.Services
    .AddDataAccess(builder.Configuration)
    .AddDataSync(builder.Configuration)
    .AddSearchProcessing(builder.Configuration)
    .AddExcelExport(builder.Configuration)
    .AddAuth(builder.Configuration);

// ASP.NET Core services
builder.Services.AddControllers();
builder.Services.AddSignalR();

var app = builder.Build();

// Middleware pipeline
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHub<StatusHub>("/statushub");

app.Run();

Validation Approach

Startup Validation

Validate critical services are registered at startup:

// In Program.cs after building
using var scope = app.Services.CreateScope();
_ = scope.ServiceProvider.GetRequiredService<ILotFinderRepository>();
_ = scope.ServiceProvider.GetRequiredService<IOptions<DataAccessOptions>>();
// ... validate other critical services

Configuration Validation

Use DataAnnotations or IValidateOptions for configuration:

public class DataAccessOptionsValidator : IValidateOptions<DataAccessOptions>
{
    public ValidateOptionsResult Validate(string? name, DataAccessOptions options)
    {
        if (options.CommandTimeoutSeconds <= 0)
            return ValidateOptionsResult.Fail("CommandTimeoutSeconds must be positive");
        return ValidateOptionsResult.Success;
    }
}