# AGENTS.md ## Build, Test & Run Commands ```bash # Build entire solution dotnet build EngineeringSync.slnx # Run the background service (dev mode) dotnet run --project EngineeringSync.Service # Run the WPF tray app dotnet run --project EngineeringSync.TrayApp # Run all tests dotnet test # Run a single test project dotnet test EngineeringSync.Tests --filter "FullyQualifiedName~WatcherServiceTests" # Run tests with verbose output dotnet test --verbosity normal # Build Release dotnet build EngineeringSync.slnx -c Release # Install as Windows Service (production) sc create EngineeringSync binPath="\EngineeringSync.Service.exe" # Build installer (requires Inno Setup 6) .\installer\build-installer.ps1 ``` ## Code Style Guidelines ### General Principles - Use C# 12+ features (file-scoped namespaces, collection expressions, primary constructors) - Use `[ObservableProperty]` and `[RelayCommand]` attributes from CommunityToolkit.Mvvm - Prefer source generators over boilerplate code ### Imports - Sort by: System namespaces first, then Microsoft, then third-party, then project-specific - Use file-scoped namespaces: `namespace EngineeringSync.Domain.Entities;` - Global using statements in each project for common types ### Types & Collections - Use collection expressions `[]` instead of `new List()` where possible - Use `IReadOnlyList` for parameters that should not be modified - Use `Guid` for all ID types - Use `DateTime.UtcNow` for timestamps, convert to local time in UI layer only ### Naming Conventions - **Classes/Interfaces:** PascalCase (e.g., `WatcherService`, `IApiClient`) - **Methods:** PascalCase (e.g., `StartWatchingAsync`) - **Private fields:** camelCase (e.g., `_watchers`, `_channel`) - **Properties:** PascalCase (e.g., `IsActive`, `EngineeringPath`) - **Parameters:** camelCase (e.g., `project`, `stoppingToken`) - **Constants:** PascalCase (e.g., `DefaultTimeout`) ### Records vs Classes - Use `record` for DTOs and API models - Use `class` for entities with mutable properties ### Error Handling - Use `try/catch` with specific exception types - Log exceptions with context using `ILogger` (use structured logging: `logger.LogError(ex, "Message {Param}", param)`) - In ViewModels, catch exceptions and set `StatusMessage` for user feedback ### Async/Await - Always use `Async` suffix for async methods - Pass `CancellationToken` to all cancellable operations - Use `await using` for `IDisposable` resources that implement `IAsyncDisposable` ### Entity Framework Core - Use `IDbContextFactory` for scoped DbContext in background services - Use `await using var db = await dbFactory.CreateDbContextAsync(ct);` - Configure SQLite with WAL mode for better concurrency ### WPF / MVVM - ViewModels inherit from `ObservableObject` (CommunityToolkit.Mvvm) - Use `[ObservableProperty]` for properties that need change notification - Use `[RelayCommand]` for commands - Use `partial class` for generated properties ### Dependency Injection - Constructor injection with primary constructors: ```csharp public sealed class WatcherService( IDbContextFactory dbFactory, IHubContext hub, ILogger logger) : BackgroundService ``` ### File Organization ``` EngineeringSync.Domain/ - Entities, Enums, Interfaces EngineeringSync.Infrastructure/ - EF Core, DbContext, Migrations EngineeringSync.Service/ - API, Hubs, Background Services EngineeringSync.TrayApp/ - WPF Views, ViewModels, Services EngineeringSync.Setup/ - Installer wizard ``` ### Key Patterns **FileSystemWatcher Debouncing:** Events flow into a `Channel`, consumer groups by `(ProjectId, RelativePath)` within 2000ms window, SHA-256 confirms actual changes. **SignalR Notifications:** `NotificationHub` at `/notifications` broadcasts `ReceiveChangeNotification(projectId, projectName, count)` to all clients. **Backup before sync:** Use timestamped naming `{filename}_{yyyyMMdd_HHmmss}.bak`. ### Service API Endpoints - `GET/POST/PUT/DELETE /api/projects` - Project CRUD - `GET /api/changes/{projectId}` - Get pending changes - `GET /api/changes/{projectId}/history` - Get synced/ignored changes - `POST /api/sync` - Sync selected changes - `POST /api/ignore` - Ignore selected changes ### Database - SQLite with WAL mode - Entities: `ProjectConfig`, `FileRevision`, `PendingChange` - `ProjectConfig.FileExtensions` is comma-separated (e.g., ".jt,.cojt,.xml") ## Solution Structure ``` EngineeringSync.slnx ├── EngineeringSync.Domain/ (Class Library, net10.0) - Entities, Enums, Interfaces ├── EngineeringSync.Infrastructure/ (Class Library, net10.0) - EF Core, DbContext ├── EngineeringSync.Service/ (Worker Service, net10.0) - Kestrel on :5050, SignalR hub ├── EngineeringSync.TrayApp/ (WPF App, net10.0-windows) - System tray + UI windows ├── EngineeringSync.Setup/ (WPF App, net10.0-windows) - Setup wizard + Installer └── installer/ ├── setup.iss - Inno Setup Script └── build-installer.ps1 - Build + Publish + ISCC Pipeline ``` ## Tech Stack - **Framework:** .NET 10 - **Service:** Worker Service (Windows Service) + ASP.NET Core Minimal API + SignalR - **Client:** WPF (.NET 10-Windows) with `H.NotifyIcon.Wpf` + `CommunityToolkit.Mvvm` - **Database:** SQLite via EF Core 10 (WAL mode) - **File Watching:** `System.IO.FileSystemWatcher` + `System.Threading.Channels` (debouncing) ## Key Architecture - **ProjectConfig in DB:** Managed via TrayApp UI, stored in SQLite. User adds/edits/deletes projects with folder browser dialogs. CRUD operations go through Service API, which dynamically starts/stops watchers. - **FileSystemWatcher Debouncing:** Events (Created, Changed, Renamed, Deleted) pushed into `Channel`. Consumer groups events by `(ProjectId, RelativePath)` within 2000ms sliding window. SHA-256 hashing against `FileRevision` confirms actual changes before writing a `PendingChange`. - **SignalR Hub:** `NotificationHub` at `/notifications` broadcasts `ReceiveChangeNotification(projectId, projectName, count)` and `ProjectConfigChanged()`. ## Notes - Domain project has no dependencies (pure entities/enums) - Infrastructure depends on Domain - Service depends on Domain + Infrastructure - TrayApp depends on Domain + Service (via HTTP client)