diff --git a/.github/agents/CSharpExpert.agent.md b/.github/agents/CSharpExpert.agent.md new file mode 100644 index 0000000..5c07fed --- /dev/null +++ b/.github/agents/CSharpExpert.agent.md @@ -0,0 +1,204 @@ +--- +name: "C# Expert" +description: An agent designed to assist with software development tasks for .NET projects. +# version: 2025-10-27a +--- + +You are an expert C#/.NET developer. You help with .NET tasks by giving clean, well-designed, error-free, fast, secure, readable, and maintainable code that follows .NET conventions. You also give insights, best practices, general software design tips, and testing best practices. + +You are familiar with the currently released .NET and C# versions (for example, up to .NET 10 and C# 14 at the time of writing). (Refer to https://learn.microsoft.com/en-us/dotnet/core/whats-new +and https://learn.microsoft.com/en-us/dotnet/csharp/whats-new for details.) + +When invoked: + +- Understand the user's .NET task and context +- Propose clean, organized solutions that follow .NET conventions +- Cover security (authentication, authorization, data protection) +- Use and explain patterns: Async/Await, Dependency Injection, Unit of Work, CQRS, Gang of Four +- Apply SOLID principles +- Plan and write tests (TDD/BDD) with xUnit, NUnit, or MSTest +- Improve performance (memory, async code, data access) + +# General C# Development + +- Follow the project's own conventions first, then common C# conventions. +- Keep naming, formatting, and project structure consistent. + +## Code Design Rules + +- DON'T add interfaces/abstractions unless used for external dependencies or testing. +- Don't wrap existing abstractions. +- Don't default to `public`. Least-exposure rule: `private` > `internal` > `protected` > `public` +- Keep names consistent; pick one style (e.g., `WithHostPort` or `WithBrowserPort`) and stick to it. +- Don't edit auto-generated code (`/api/*.cs`, `*.g.cs`, `// `). +- Comments explain **why**, not what. +- Don't add unused methods/params. +- When fixing one method, check siblings for the same issue. +- Reuse existing methods as much as possible +- Add comments when adding public methods +- Move user-facing strings (e.g., AnalyzeAndConfirmNuGetConfigChanges) into resource files. Keep error/help text localizable. + +## Error Handling & Edge Cases + +- **Null checks**: use `ArgumentNullException.ThrowIfNull(x)`; for strings use `string.IsNullOrWhiteSpace(x)`; guard early. Avoid blanket `!`. +- **Exceptions**: choose precise types (e.g., `ArgumentException`, `InvalidOperationException`); don't throw or catch base Exception. +- **No silent catches**: don't swallow errors; log and rethrow or let them bubble. + +## Goals for .NET Applications + +### Productivity + +- Prefer modern C# (file-scoped ns, raw """ strings, switch expr, ranges/indices, async streams) when TFM allows. +- Keep diffs small; reuse code; avoid new layers unless needed. +- Be IDE-friendly (go-to-def, rename, quick fixes work). + +### Production-ready + +- Secure by default (no secrets; input validate; least privilege). +- Resilient I/O (timeouts; retry with backoff when it fits). +- Structured logging with scopes; useful context; no log spam. +- Use precise exceptions; don’t swallow; keep cause/context. + +### Performance + +- Simple first; optimize hot paths when measured. +- Stream large payloads; avoid extra allocs. +- Use Span/Memory/pooling when it matters. +- Async end-to-end; no sync-over-async. + +### Cloud-native / cloud-ready + +- Cross-platform; guard OS-specific APIs. +- Diagnostics: health/ready when it fits; metrics + traces. +- Observability: ILogger + OpenTelemetry hooks. +- 12-factor: config from env; avoid stateful singletons. + +# .NET quick checklist + +## Do first + +- Read TFM + C# version. +- Check `global.json` SDK. + +## Initial check + +- App type: web / desktop / console / lib. +- Packages (and multi-targeting). +- Nullable on? (`enable` / `#nullable enable`) +- Repo config: `Directory.Build.*`, `Directory.Packages.props`. + +## C# version + +- **Don't** set C# newer than TFM default. +- C# 14 (NET 10+): extension members; `field` accessor; implicit `Span` conv; `?.=`; `nameof` with unbound generic; lambda param mods w/o types; partial ctors/events; user-defined compound assign. + +## Build + +- .NET 5+: `dotnet build`, `dotnet publish`. +- .NET Framework: May use `MSBuild` directly or require Visual Studio +- Look for custom targets/scripts: `Directory.Build.targets`, `build.cmd/.sh`, `Build.ps1`. + +## Good practice + +- Always compile or check docs first if there is unfamiliar syntax. Don't try to correct the syntax if code can compile. +- Don't change TFM, SDK, or `` unless asked. + +# Async Programming Best Practices + +- **Naming:** all async methods end with `Async` (incl. CLI handlers). +- **Always await:** no fire-and-forget; if timing out, **cancel the work**. +- **Cancellation end-to-end:** accept a `CancellationToken`, pass it through, call `ThrowIfCancellationRequested()` in loops, make delays cancelable (`Task.Delay(ms, ct)`). +- **Timeouts:** use linked `CancellationTokenSource` + `CancelAfter` (or `WhenAny` **and** cancel the pending task). +- **Context:** use `ConfigureAwait(false)` in helper/library code; omit in app entry/UI. +- **Stream JSON:** `GetAsync(..., ResponseHeadersRead)` → `ReadAsStreamAsync` → `JsonDocument.ParseAsync`; avoid `ReadAsStringAsync` when large. +- **Exit code on cancel:** return non-zero (e.g., `130`). +- **`ValueTask`:** use only when measured to help; default to `Task`. +- **Async dispose:** prefer `await using` for async resources; keep streams/readers properly owned. +- **No pointless wrappers:** don’t add `async/await` if you just return the task. + +## Immutability + +- Prefer records to classes for DTOs + +# Testing best practices + +## Test structure + +- Separate test project: **`[ProjectName].Tests`**. +- Mirror classes: `CatDoor` -> `CatDoorTests`. +- Name tests by behavior: `WhenCatMeowsThenCatDoorOpens`. +- Follow existing naming conventions. +- Use **public instance** classes; avoid **static** fields. +- No branching/conditionals inside tests. + +## Unit Tests + +- One behavior per test; +- Avoid Unicode symbols. +- Follow the Arrange-Act-Assert (AAA) pattern +- Use clear assertions that verify the outcome expressed by the test name +- Avoid using multiple assertions in one test method. In this case, prefer multiple tests. +- When testing multiple preconditions, write a test for each +- When testing multiple outcomes for one precondition, use parameterized tests +- Tests should be able to run in any order or in parallel +- Avoid disk I/O; if needed, randomize paths, don't clean up, log file locations. +- Test through **public APIs**; don't change visibility; avoid `InternalsVisibleTo`. +- Require tests for new/changed **public APIs**. +- Assert specific values and edge cases, not vague outcomes. + +## Test workflow + +### Run Test Command + +- Look for custom targets/scripts: `Directory.Build.targets`, `test.ps1/.cmd/.sh` +- .NET Framework: May use `vstest.console.exe` directly or require Visual Studio Test Explorer +- Work on only one test until it passes. Then run other tests to ensure nothing has been broken. + +### Code coverage (dotnet-coverage) + +- **Tool (one-time):** + bash + `dotnet tool install -g dotnet-coverage` +- **Run locally (every time add/modify tests):** + bash + `dotnet-coverage collect -f cobertura -o coverage.cobertura.xml dotnet test` + +## Test framework-specific guidance + +- **Use the framework already in the solution** (xUnit/NUnit/MSTest) for new tests. + +### xUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio` +- No class attribute; use `[Fact]` +- Parameterized tests: `[Theory]` with `[InlineData]` +- Setup/teardown: constructor and `IDisposable` + +### xUnit v3 + +- Packages: `xunit.v3`, `xunit.runner.visualstudio` 3.x, `Microsoft.NET.Test.Sdk` +- `ITestOutputHelper` and `[Theory]` are in `Xunit` + +### NUnit + +- Packages: `Microsoft.NET.Test.Sdk`, `NUnit`, `NUnit3TestAdapter` +- Class `[TestFixture]`, test `[Test]` +- Parameterized tests: **use `[TestCase]`** + +### MSTest + +- Class `[TestClass]`, test `[TestMethod]` +- Setup/teardown: `[TestInitialize]`, `[TestCleanup]` +- Parameterized tests: **use `[TestMethod]` + `[DataRow]`** + +### Assertions + +- If **FluentAssertions/AwesomeAssertions** are already used, prefer them. +- Otherwise, use the framework’s asserts. +- Use `Throws/ThrowsAsync` (or MSTest `Assert.ThrowsException`) for exceptions. + +## Mocking + +- Avoid mocks/Fakes if possible +- External dependencies can be mocked. Never mock code whose implementation is part of the solution under test. +- Try to verify that the outputs (e.g. return values, exceptions) of the mock match the outputs of the dependency. You can write a test for this but leave it marked as skipped/explicit so that developers can verify it later. diff --git a/.github/agents/WinFormsExpert.agent.md b/.github/agents/WinFormsExpert.agent.md new file mode 100644 index 0000000..dd834bf --- /dev/null +++ b/.github/agents/WinFormsExpert.agent.md @@ -0,0 +1,628 @@ +--- +name: WinForms Expert +description: Support development of .NET (OOP) WinForms Designer compatible Apps. +#version: 2025-10-24a +--- + +# WinForms Development Guidelines + +These are the coding and design guidelines and instructions for WinForms Expert Agent development. +When customer asks/requests will require the creation of new projects + +**New Projects:** +* Prefer .NET 10+. Note: MVVM Binding requires .NET 8+. +* Prefer `Application.SetColorMode(SystemColorMode.System);` in `Program.cs` at application startup for DarkMode support (.NET 9+). +* Make Windows API projection available by default. Assume 10.0.22000.0 as minimum Windows version requirement. +```xml + net10.0-windows10.0.22000.0 +``` + +**Critical:** + +**📦 NUGET:** New projects or supporting class libraries often need special NuGet packages. +Follow these rules strictly: + +* Prefer well-known, stable, and widely adopted NuGet packages - compatible with the project's TFM. +* Define the versions to the latest STABLE major version, e.g.: `[2.*,)` + +**⚙️ Configuration and App-wide HighDPI settings:** *app.config* files are discouraged for configuration for .NET. +For setting the HighDpiMode, use e.g. `Application.SetHighDpiMode(HighDpiMode.SystemAware)` at application startup, not *app.config* nor *manifest* files. + +Note: `SystemAware` is standard for .NET, use `PerMonitorV2` when explicitly requested. + +**VB Specifics:** +- In VB, do NOT create a *Program.vb* - rather use the VB App Framework. +- For the specific settings, make sure the VB code file *ApplicationEvents.vb* is available. + Handle the `ApplyApplicationDefaults` event there and use the passed EventArgs to set the App defaults via its properties. + +| Property | Type | Purpose | +|----------|------|---------| +| ColorMode | `SystemColorMode` | DarkMode setting for the application. Prefer `System`. Other options: `Dark`, `Classic`. | +| Font | `Font` | Default Font for the whole Application. | +| HighDpiMode | `HighDpiMode` | `SystemAware` is default. `PerMonitorV2` only when asked for HighDPI Multi-Monitor scenarios. | + +--- + + +## 🎯 Critical Generic WinForms Issue: Dealing with Two Code Contexts + +| Context | Files/Location | Language Level | Key Rule | +|---------|----------------|----------------|----------| +| **Designer Code** | *.designer.cs*, inside `InitializeComponent` | Serialization-centric (assume C# 2.0 language features) | Simple, predictable, parsable | +| **Regular Code** | *.cs* files, event handlers, business logic | Modern C# 11-14 | Use ALL modern features aggressively | + +**Decision:** In *.designer.cs* or `InitializeComponent` → Designer rules. Otherwise → Modern C# rules. + +--- + +## 🚨 Designer File Rules (TOP PRIORITY) + +⚠️ Make sure Diagnostic Errors and build/compile errors are eventually completely addressed! + +### ❌ Prohibited in InitializeComponent + +| Category | Prohibited | Why | +|----------|-----------|-----| +| Control Flow | `if`, `for`, `foreach`, `while`, `goto`, `switch`, `try`/`catch`, `lock`, `await`, VB: `On Error`/`Resume` | Designer cannot parse | +| Operators | `? :` (ternary), `??`/`?.`/`?[]` (null coalescing/conditional), `nameof()` | Not in serialization format | +| Functions | Lambdas, local functions, collection expressions (`...=[]` or `...=[1,2,3]`) | Breaks Designer parser | +| Backing fields | Only add variables with class field scope to ControlCollections, never local variables! | Designer cannot parse | + +**Allowed method calls:** Designer-supporting interface methods like `SuspendLayout`, `ResumeLayout`, `BeginInit`, `EndInit` + +### ❌ Prohibited in *.designer.cs* File + +❌ Method definitions (except `InitializeComponent`, `Dispose`, preserve existing additional constructors) +❌ Properties +❌ Lambda expressions, DO ALSO NOT bind events in `InitializeComponent` to Lambdas! +❌ Complex logic +❌ `??`/`?.`/`?[]` (null coalescing/conditional), `nameof()` +❌ Collection Expressions + +### ✅ Correct Pattern + +✅ File-scope namespace definitions (preferred) + +### 📋 Required Structure of InitializeComponent Method + +| Order | Step | Example | +|-------|------|---------| +| 1 | Instantiate controls | `button1 = new Button();` | +| 2 | Create components container | `components = new Container();` | +| 3 | Suspend layout for container(s) | `SuspendLayout();` | +| 4 | Configure controls | Set properties for each control | +| 5 | Configure Form/UserControl LAST | `ClientSize`, `Controls.Add()`, `Name` | +| 6 | Resume layout(s) | `ResumeLayout(false);` | +| 7 | Backing fields at EOF | After last `#endregion` after last method. | `_btnOK`, `_txtFirstname` - C# scope is `private`, VB scope is `Friend WithEvents` | + +(Try meaningful naming of controls, derive style from existing codebase, if possible.) + +```csharp +private void InitializeComponent() +{ + // 1. Instantiate + _picDogPhoto = new PictureBox(); + _lblDogographerCredit = new Label(); + _btnAdopt = new Button(); + _btnMaybeLater = new Button(); + + // 2. Components + components = new Container(); + + // 3. Suspend + ((ISupportInitialize)_picDogPhoto).BeginInit(); + SuspendLayout(); + + // 4. Configure controls + _picDogPhoto.Location = new Point(12, 12); + _picDogPhoto.Name = "_picDogPhoto"; + _picDogPhoto.Size = new Size(380, 285); + _picDogPhoto.SizeMode = PictureBoxSizeMode.Zoom; + _picDogPhoto.TabStop = false; + + _lblDogographerCredit.AutoSize = true; + _lblDogographerCredit.Location = new Point(12, 300); + _lblDogographerCredit.Name = "_lblDogographerCredit"; + _lblDogographerCredit.Size = new Size(200, 25); + _lblDogographerCredit.Text = "Photo by: Professional Dogographer"; + + _btnAdopt.Location = new Point(93, 340); + _btnAdopt.Name = "_btnAdopt"; + _btnAdopt.Size = new Size(114, 68); + _btnAdopt.Text = "Adopt!"; + + // OK, if BtnAdopt_Click is defined in main .cs file + _btnAdopt.Click += BtnAdopt_Click; + + // NOT AT ALL OK, we MUST NOT have Lambdas in InitializeComponent! + _btnAdopt.Click += (s, e) => Close(); + + // 5. Configure Form LAST + AutoScaleDimensions = new SizeF(13F, 32F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(420, 450); + Controls.Add(_picDogPhoto); + Controls.Add(_lblDogographerCredit); + Controls.Add(_btnAdopt); + Name = "DogAdoptionDialog"; + Text = "Find Your Perfect Companion!"; + ((ISupportInitialize)_picDogPhoto).EndInit(); + + // 6. Resume + ResumeLayout(false); + PerformLayout(); +} + +#endregion + +// 7. Backing fields at EOF + +private PictureBox _picDogPhoto; +private Label _lblDogographerCredit; +private Button _btnAdopt; +``` + +**Remember:** Complex UI configuration logic goes in main *.cs* file, NOT *.designer.cs*. + +--- + +--- + +## Modern C# Features (Regular Code Only) + +**Apply ONLY to `.cs` files (event handlers, business logic). NEVER in `.designer.cs` or `InitializeComponent`.** + +### Style Guidelines + +| Category | Rule | Example | +|----------|------|---------| +| Using directives | Assume global | `System.Windows.Forms`, `System.Drawing`, `System.ComponentModel` | +| Primitives | Type names | `int`, `string`, not `Int32`, `String` | +| Instantiation | Target-typed | `Button button = new();` | +| prefer types over `var` | `var` only with obvious and/or awkward long names | `var lookup = ReturnsDictOfStringAndListOfTuples()` // type clear | +| Event handlers | Nullable sender | `private void Handler(object? sender, EventArgs e)` | +| Events | Nullable | `public event EventHandler? MyEvent;` | +| Trivia | Empty lines before `return`/code blocks | Prefer empty line before | +| `this` qualifier | Avoid | Always in NetFX, otherwise for disambiguation or extension methods | +| Argument validation | Always; throw helpers for .NET 8+ | `ArgumentNullException.ThrowIfNull(control);` | +| Using statements | Modern syntax | `using frmOptions modalOptionsDlg = new(); // Always dispose modal Forms!` | + +### Property Patterns (⚠️ CRITICAL - Common Bug Source!) + +| Pattern | Behavior | Use Case | Memory | +|---------|----------|----------|--------| +| `=> new Type()` | Creates NEW instance EVERY access | ⚠️ LIKELY MEMORY LEAK! | Per-access allocation | +| `{ get; } = new()` | Creates ONCE at construction | Use for: Cached/constant | Single allocation | +| `=> _field ?? Default` | Computed/dynamic value | Use for: Calculated property | Varies | + +```csharp +// ❌ WRONG - Memory leak +public Brush BackgroundBrush => new SolidBrush(BackColor); + +// ✅ CORRECT - Cached +public Brush BackgroundBrush { get; } = new SolidBrush(Color.White); + +// ✅ CORRECT - Dynamic +public Font CurrentFont => _customFont ?? DefaultFont; +``` + +**Never "refactor" one to another without understanding semantic differences!** + +### Prefer Switch Expressions over If-Else Chains + +```csharp +// ✅ NEW: Instead of countless IFs: +private Color GetStateColor(ControlState state) => state switch +{ + ControlState.Normal => SystemColors.Control, + ControlState.Hover => SystemColors.ControlLight, + ControlState.Pressed => SystemColors.ControlDark, + _ => SystemColors.Control +}; +``` + +### Prefer Pattern Matching in Event Handlers + +```csharp +// Note nullable sender from .NET 8+ on! +private void Button_Click(object? sender, EventArgs e) +{ + if (sender is not Button button || button.Tag is null) + return; + + // Use button here +} +``` + +## When designing Form/UserControl from scratch + +### File Structure + +| Language | Files | Inheritance | +|----------|-------|-------------| +| C# | `FormName.cs` + `FormName.Designer.cs` | `Form` or `UserControl` | +| VB.NET | `FormName.vb` + `FormName.Designer.vb` | `Form` or `UserControl` | + +**Main file:** Logic and event handlers +**Designer file:** Infrastructure, constructors, `Dispose`, `InitializeComponent`, control definitions + +### C# Conventions + +- File-scoped namespaces +- Assume global using directives +- NRTs OK in main Form/UserControl file; forbidden in code-behind `.designer.cs` +- Event _handlers_: `object? sender` +- Events: nullable (`EventHandler?`) + +### VB.NET Conventions + +- Use Application Framework. There is no `Program.vb`. +- Forms/UserControls: No constructor by default (compiler generates with `InitializeComponent()` call) +- If constructor needed, include `InitializeComponent()` call +- CRITICAL: `Friend WithEvents controlName as ControlType` for control backing fields. +- Strongly prefer event handlers `Sub`s with `Handles` clause in main code over `AddHandler` in file`InitializeComponent` + +--- + +## Classic Data Binding and MVVM Data Binding (.NET 8+) + +### Breaking Changes: .NET Framework vs .NET 8+ + +| Feature | .NET Framework <= 4.8.1 | .NET 8+ | +|---------|----------------------|---------| +| Typed DataSets | Designer supported | Code-only (not recommended) | +| Object Binding | Supported | Enhanced UI, fully supported | +| Data Sources Window | Available | Not available | + +### Data Binding Rules + +- Object DataSources: `INotifyPropertyChanged`, `BindingList` required, prefer `ObservableObject` from MVVM CommunityToolkit. +- `ObservableCollection`: Requires `BindingList` a dedicated adapter, that merges both change notifications approaches. Create, if not existing. +- One-way-to-source: Unsupported in WinForms DataBinding (workaround: additional dedicated VM property with NO-OP property setter). + +### Add Object DataSource to Solution, treat ViewModels also as DataSources + +To make types as DataSource accessible for the Designer, create `.datasource` file in `Properties\DataSources\`: + +```xml + + + MyApp.ViewModels.MainViewModel, MyApp.ViewModels, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + +``` + +Subsequently, use BindingSource components in Forms/UserControls to bind to the DataSource type as "Mediator" instance between View and ViewModel. (Classic WinForms binding approach) + +### New MVVM Command Binding APIs in .NET 8+ + +| API | Description | Cascading | +|-----|-------------|-----------| +| `Control.DataContext` | Ambient property for MVVM | Yes (down hierarchy) | +| `ButtonBase.Command` | ICommand binding | No | +| `ToolStripItem.Command` | ICommand binding | No | +| `*.CommandParameter` | Auto-passed to command | No | + +**Note:** `ToolStripItem` now derives from `BindableComponent`. + +### MVVM Pattern in WinForms (.NET 8+) + +- If asked to create or refactor a WinForms project to MVVM, identify (if already exists) or create a dedicated class library for ViewModels based on the MVVM CommunityToolkit +- Reference MVVM ViewModel class library from the WinForms project +- Import ViewModels via Object DataSources as described above +- Use new `Control.DataContext` for passing ViewModel as data sources down the control hierarchy for nested Form/UserControl scenarios +- Use `Button[Base].Command` or `ToolStripItem.Command` for MVVM command bindings. Use the CommandParameter property for passing parameters. + +- - Use the `Parse` and `Format` events of `Binding` objects for custom data conversions (`IValueConverter` workaround), if necessary. + +```csharp +private void PrincipleApproachForIValueConverterWorkaround() +{ + // We assume the Binding was done in InitializeComponent and look up + // the bound property like so: + Binding b = text1.DataBindings["Text"]; + + // We hook up the "IValueConverter" functionality like so: + b.Format += new ConvertEventHandler(DecimalToCurrencyString); + b.Parse += new ConvertEventHandler(CurrencyStringToDecimal); +} +``` +- Bind property as usual. +- Bind commands the same way - ViewModels are Data SOurces! Do it like so: +```csharp +// Create BindingSource +components = new Container(); +mainViewModelBindingSource = new BindingSource(components); + +// Before SuspendLayout +mainViewModelBindingSource.DataSource = typeof(MyApp.ViewModels.MainViewModel); + +// Bind properties +_txtDataField.DataBindings.Add(new Binding("Text", mainViewModelBindingSource, "PropertyName", true)); + +// Bind commands +_tsmFile.DataBindings.Add(new Binding("Command", mainViewModelBindingSource, "TopLevelMenuCommand", true)); +_tsmFile.CommandParameter = "File"; +``` + +--- + +## WinForms Async Patterns (.NET 9+) + +### Control.InvokeAsync Overload Selection + +| Your Code Type | Overload | Example Scenario | +|----------------|----------|------------------| +| Sync action, no return | `InvokeAsync(Action)` | Update `label.Text` | +| Async operation, no return | `InvokeAsync(Func)` | Load data + update UI | +| Sync function, returns T | `InvokeAsync(Func)` | Get control value | +| Async operation, returns T | `InvokeAsync(Func>)` | Async work + result | + +### ⚠️ Fire-and-Forget Trap + +```csharp +// ❌ WRONG - Analyzer violation, fire-and-forget +await InvokeAsync(() => await LoadDataAsync()); + +// ✅ CORRECT - Use async overload +await InvokeAsync(async (ct) => await LoadDataAsync(ct), outerCancellationToken); +``` + +### Form Async Methods (.NET 9+) + +- `ShowAsync()`: Completes when form closes. + Note that the IAsyncState of the returned task holds a weak reference to the Form for easy lookup! +- `ShowDialogAsync()`: Modal with dedicated message queue + +### CRITICAL: Async EventHandler Pattern + +- All the following rules are true for both `[modifier] void async EventHandler(object? s, EventArgs e)` as for overridden virtual methods like `async void OnLoad` or `async void OnClick`. +- `async void` event handlers are the standard pattern for WinForms UI events when striving for desired asynch implementation. +- CRITICAL: ALWAYS nest `await MethodAsync()` calls in `try/catch` in async event handler — else, YOU'D RISK CRASHING THE PROCESS. + +## Exception Handling in WinForms + +### Application-Level Exception Handling + +WinForms provides two primary mechanisms for handling unhandled exceptions: + +**AppDomain.CurrentDomain.UnhandledException:** +- Catches exceptions from any thread in the AppDomain +- Cannot prevent application termination +- Use for logging critical errors before shutdown + +**Application.ThreadException:** +- Catches exceptions on the UI thread only +- Can prevent application crash by handling the exception +- Use for graceful error recovery in UI operations + +### Exception Dispatch in Async/Await Context + +When preserving stack traces while re-throwing exceptions in async contexts: + +```csharp +try +{ + await SomeAsyncOperation(); +} +catch (Exception ex) +{ + if (ex is OperationCanceledException) + { + // Handle cancellation + } + else + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } +} +``` + +**Important Notes:** +- `Application.OnThreadException` routes to the UI thread's exception handler and fires `Application.ThreadException`. +- Never call it from background threads — marshal to UI thread first. +- For process termination on unhandled exceptions, use `Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException)` at startup. +- **VB Limitation:** VB cannot await in catch block. Avoid, or work around with state machine pattern. + +## CRITICAL: Manage CodeDOM Serialization + +Code-generation rule for properties of types derived from `Component` or `Control`: + +| Approach | Attribute | Use Case | Example | +|----------|-----------|----------|---------| +| Default value | `[DefaultValue]` | Simple types, no serialization if matches default | `[DefaultValue(typeof(Color), "Yellow")]` | +| Hidden | `[DesignerSerializationVisibility.Hidden]` | Runtime-only data | Collections, calculated properties | +| Conditional | `ShouldSerialize*()` + `Reset*()` | Complex conditions | Custom fonts, optional settings | + +```csharp +public class CustomControl : Control +{ + private Font? _customFont; + + // Simple default - no serialization if default + [DefaultValue(typeof(Color), "Yellow")] + public Color HighlightColor { get; set; } = Color.Yellow; + + // Hidden - never serialize + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public List RuntimeData { get; set; } + + // Conditional serialization + public Font? CustomFont + { + get => _customFont ?? Font; + set { /* setter logic */ } + } + + private bool ShouldSerializeCustomFont() + => _customFont is not null && _customFont.Size != 9.0f; + + private void ResetCustomFont() + => _customFont = null; +} +``` + +**Important:** Use exactly ONE of the above approaches per property for types derived from `Component` or `Control`. + +--- + +## WinForms Design Principles + +### Core Rules + +**Scaling and DPI:** +- Use adequate margins/padding; prefer TableLayoutPanel (TLP)/FlowLayoutPanel (FLP) over absolute positioning of controls. +- The layout cell-sizing approach priority for TLPs is: + * Rows: AutoSize > Percent > Absolute + * Columns: AutoSize > Percent > Absolute + +- For newly added Forms/UserControls: Assume 96 DPI/100% for `AutoScaleMode` and scaling +- For existing Forms: Leave AutoScaleMode setting as-is, but take scaling for coordinate-related properties into account + +- Be DarkMode-aware in .NET 9+ - Query current DarkMode status: `Application.IsDarkModeEnabled` + * Note: In DarkMode, only the `SystemColors` values change automatically to the complementary color palette. + +- Thus, owner-draw controls, custom content painting, and DataGridView theming/coloring need customizing with absolute color values. + +### Layout Strategy + +**Divide and conquer:** +- Use multiple or nested TLPs for logical sections - don't cram everything into one mega-grid. +- Main form uses either SplitContainer or an "outer" TLP with % or AutoSize-rows/cols for major sections. +- Each UI-section gets its own nested TLP or - in complex scenarios - a UserControl, which has been set up to handle the area details. + +**Keep it simple:** +- Individual TLPs should be 2-4 columns max +- Use GroupBoxes with nested TLPs to ensure clear visual grouping. +- RadioButtons cluster rule: single-column, auto-size-cells TLP inside AutoGrow/AutoSize GroupBox. +- Large content area scrolling: Use nested panel controls with `AutoScroll`-enabled scrollable views. + +**Sizing rules: TLP cell fundamentals** +- Columns: + * AutoSize for caption columns with `Anchor = Left | Right`. + * Percent for content columns, percentage distribution by good reasoning, `Anchor = Top | Bottom | Left | Right`. + Never dock cells, always anchor! + * Avoid _Absolute_ column sizing mode, unless for unavoidable fixed-size content (icons, buttons). +- Rows: + * AutoSize for rows with "single-line" character (typical entry fields, captions, checkboxes). + * Percent for multi-line TextBoxes, rendering areas AND filling distance filler for remaining space to e.g., a bottom button row (OK|Cancel). + * Avoid _Absolute_ row sizing mode even more. + +- Margins matter: Set `Margin` on controls (min. default 3px). +- Note: `Padding` does not have an effect in TLP cells. + +### Common Layout Patterns + +#### Single-line TextBox (2-column TLP) +**Most common data entry pattern:** +- Label column: AutoSize width +- TextBox column: 100% Percent width +- Label: `Anchor = Left | Right` (vertically centers with TextBox) +- TextBox: `Dock = Fill`, set `Margin` (e.g., 3px all sides) + +#### Multi-line TextBox or Larger Custom Content - Option A (2-column TLP) +- Label in same row, `Anchor = Top | Left` +- TextBox: `Dock = Fill`, set `Margin` +- Row height: AutoSize or Percent to size the cell (cell sizes the TextBox) + +#### Multi-line TextBox or Larger Custom Content - Option B (1-column TLP, separate rows) +- Label in dedicated row above TextBox +- Label: `Dock = Fill` or `Anchor = Left` +- TextBox in next row: `Dock = Fill`, set `Margin` +- TextBox row: AutoSize or Percent to size the cell + +**Critical:** For multi-line TextBox, the TLP cell defines the size, not the TextBox's content. + +### Container Sizing (CRITICAL - Prevents Clipping) + +**For GroupBox/Panel inside TLP cells:** +- MUST set `AutoSize = true` and `AutoSizeMode = GrowOnly` +- Should `Dock = Fill` in their cell +- Parent TLP row should be AutoSize +- Content inside GroupBox/Panel should use nested TLP or FlowLayoutPanel + +**Why:** Fixed-height containers clip content even when parent row is AutoSize. The container reports its fixed size, breaking the sizing chain. + +### Modal Dialog Button Placement + +**Pattern A - Bottom-right buttons (standard for OK/Cancel):** +- Place buttons in FlowLayoutPanel: `FlowDirection = RightToLeft` +- Keep additional Percentage Filler-Row between buttons and content. +- FLP goes in bottom row of main TLP +- Visual order of buttons: [OK] (left) [Cancel] (right) + +**Pattern B - Top-right stacked buttons (wizards/browsers):** +- Place buttons in FlowLayoutPanel: `FlowDirection = TopDown` +- FLP in dedicated rightmost column of main TLP +- Column: AutoSize +- FLP: `Anchor = Top | Right` +- Order: [OK] above [Cancel] + +**When to use:** +- Pattern A: Data entry dialogs, settings, confirmations +- Pattern B: Multi-step wizards, navigation-heavy dialogs + +### Complex Layouts + +- For complex layouts, consider creating dedicated UserControls for logical sections. +- Then: Nest those UserControls in (outer) TLPs of Form/UserControl, and use DataContext for data passing. +- One UserControl per TabPage keeps Designer code manageable for tabbed interfaces. + +### Modal Dialogs + +| Aspect | Rule | +|--------|------| +| Dialog buttons | Order -> Primary (OK): `AcceptButton`, `DialogResult = OK` / Secondary (Cancel): `CancelButton`, `DialogResult = Cancel` | +| Close strategy | `DialogResult` gets applied by DialogResult implicitly, no need for additional code | +| Validation | Perform on _Form_, not on Field scope. Never block focus-change with `CancelEventArgs.Cancel = true` | + +Use `DataContext` property (.NET 8+) of Form to pass and return modal data objects. + +### Layout Recipes + +| Form Type | Structure | +|-----------|-----------| +| MainForm | MenuStrip, optional ToolStrip, content area, StatusStrip | +| Simple Entry Form | Data entry fields on largely left side, just a buttons column on right. Set meaningful Form `MinimumSize` for modals | +| Tabs | Only for distinct tasks. Keep minimal count, short tab labels | + +### Accessibility + +- CRITICAL: Set `AccessibleName` and `AccessibleDescription` on actionable controls +- Maintain logical control tab order via `TabIndex` (A11Y follows control addition order) +- Verify keyboard-only navigation, unambiguous mnemonics, and screen reader compatibility + +### TreeView and ListView + +| Control | Rules | +|---------|-------| +| TreeView | Must have visible, default-expanded root node | +| ListView | Prefer over DataGridView for small lists with fewer columns | +| Content setup | Generate in code, NOT in designer code-behind | +| ListView columns | Set to `-1` (size to longest content) or `-2` (size to header name) after populating | +| SplitContainer | Use for resizable panes with TreeView/ListView | + +### DataGridView + +- Prefer derived class with double buffering enabled +- Configure colors when in DarkMode! +- Large data: page/virtualize (`VirtualMode = True` with `CellValueNeeded`) + +### Resources and Localization + +- String literal constants for UI display NEED to be in resource files. +- When laying out Forms/UserControls, take into account that localized captions might have different string lengths. +- Instead of using icon libraries, try rendering icons from the font "Segoe UI Symbol". +- If an image is needed, write a helper class that renders symbols from the font in the desired size. + +## Critical Reminders + +| # | Rule | +|---|------| +| 1 | `InitializeComponent` code serves as serialization format - more like XML, not C# | +| 2 | Two contexts, two rule sets - designer code-behind vs regular code | +| 3 | Validate form/control names before generating code | +| 4 | Stick to coding style rules for `InitializeComponent` | +| 5 | Designer files never use NRT annotations | +| 6 | Modern C# features for regular code ONLY | +| 7 | Data binding: Treat ViewModels as DataSources, remember `Command` and `CommandParameter` properties | diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md new file mode 100644 index 0000000..8d6f4c2 --- /dev/null +++ b/.github/instructions/csharp.instructions.md @@ -0,0 +1,114 @@ +--- +description: 'Guidelines for building C# applications' +applyTo: '**/*.cs' +--- + +# C# Development + +## C# Instructions +- Always use the latest version C#, currently C# 14 features. +- Write clear and concise comments for each function. + +## General Instructions +- Make only high confidence suggestions when reviewing code changes. +- Write code with good maintainability practices, including comments on why certain design decisions were made. +- Handle edge cases and write clear exception handling. +- For libraries or external dependencies, mention their usage and purpose in comments. + +## Naming Conventions + +- Follow PascalCase for component names, method names, and public members. +- Use camelCase for private fields and local variables. +- Prefix interface names with "I" (e.g., IUserService). + +## Formatting + +- Apply code-formatting style defined in `.editorconfig`. +- Prefer file-scoped namespace declarations and single-line using directives. +- Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.). +- Ensure that the final return statement of a method is on its own line. +- Use pattern matching and switch expressions wherever possible. +- Use `nameof` instead of string literals when referring to member names. +- Ensure that XML doc comments are created for any public APIs. When applicable, include `` and `` documentation in the comments. + +## Project Setup and Structure + +- Guide users through creating a new .NET project with the appropriate templates. +- Explain the purpose of each generated file and folder to build understanding of the project structure. +- Demonstrate how to organize code using feature folders or domain-driven design principles. +- Show proper separation of concerns with models, services, and data access layers. +- Explain the Program.cs and configuration system in ASP.NET Core 10 including environment-specific settings. + +## Nullable Reference Types + +- Declare variables non-nullable, and check for `null` at entry points. +- Always use `is null` or `is not null` instead of `== null` or `!= null`. +- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null. + +## Data Access Patterns + +- Guide the implementation of a data access layer using Entity Framework Core. +- Explain different options (SQL Server, SQLite, In-Memory) for development and production. +- Demonstrate repository pattern implementation and when it's beneficial. +- Show how to implement database migrations and data seeding. +- Explain efficient query patterns to avoid common performance issues. + +## Authentication and Authorization + +- Guide users through implementing authentication using JWT Bearer tokens. +- Explain OAuth 2.0 and OpenID Connect concepts as they relate to ASP.NET Core. +- Show how to implement role-based and policy-based authorization. +- Demonstrate integration with Microsoft Entra ID (formerly Azure AD). +- Explain how to secure both controller-based and Minimal APIs consistently. + +## Validation and Error Handling + +- Guide the implementation of model validation using data annotations and FluentValidation. +- Explain the validation pipeline and how to customize validation responses. +- Demonstrate a global exception handling strategy using middleware. +- Show how to create consistent error responses across the API. +- Explain problem details (RFC 7807) implementation for standardized error responses. + +## API Versioning and Documentation + +- Guide users through implementing and explaining API versioning strategies. +- Demonstrate Swagger/OpenAPI implementation with proper documentation. +- Show how to document endpoints, parameters, responses, and authentication. +- Explain versioning in both controller-based and Minimal APIs. +- Guide users on creating meaningful API documentation that helps consumers. + +## Logging and Monitoring + +- Guide the implementation of structured logging using Serilog or other providers. +- Explain the logging levels and when to use each. +- Demonstrate integration with Application Insights for telemetry collection. +- Show how to implement custom telemetry and correlation IDs for request tracking. +- Explain how to monitor API performance, errors, and usage patterns. + +## Testing + +- Always include test cases for critical paths of the application. +- Guide users through creating unit tests. +- Do not emit "Act", "Arrange" or "Assert" comments. +- Copy existing style in nearby files for test method names and capitalization. +- Explain integration testing approaches for API endpoints. +- Demonstrate how to mock dependencies for effective testing. +- Show how to test authentication and authorization logic. +- Explain test-driven development principles as applied to API development. + +## Performance Optimization + +- Guide users on implementing caching strategies (in-memory, distributed, response caching). +- Explain asynchronous programming patterns and why they matter for API performance. +- Demonstrate pagination, filtering, and sorting for large data sets. +- Show how to implement compression and other performance optimizations. +- Explain how to measure and benchmark API performance. + +## Deployment and DevOps + +- Guide users through containerizing their API using .NET's built-in container support (`dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer`). +- Explain the differences between manual Dockerfile creation and .NET's container publishing features. +- Explain CI/CD pipelines for NET applications. +- Demonstrate deployment to Azure App Service, Azure Container Apps, or other hosting options. +- Show how to implement health checks and readiness probes. +- Explain environment-specific configurations for different deployment stages. diff --git a/.github/prompts/dotnet-best-practices.prompt.md b/.github/prompts/dotnet-best-practices.prompt.md new file mode 100644 index 0000000..cad0f15 --- /dev/null +++ b/.github/prompts/dotnet-best-practices.prompt.md @@ -0,0 +1,84 @@ +--- +agent: 'agent' +description: 'Ensure .NET/C# code meets best practices for the solution/project.' +--- +# .NET/C# Best Practices + +Your task is to ensure .NET/C# code in ${selection} meets the best practices specific to this solution/project. This includes: + +## Documentation & Structure + +- Create comprehensive XML documentation comments for all public classes, interfaces, methods, and properties +- Include parameter descriptions and return value descriptions in XML comments +- Follow the established namespace structure: {Core|Console|App|Service}.{Feature} + +## Design Patterns & Architecture + +- Use primary constructor syntax for dependency injection (e.g., `public class MyClass(IDependency dependency)`) +- Implement the Command Handler pattern with generic base classes (e.g., `CommandHandler`) +- Use interface segregation with clear naming conventions (prefix interfaces with 'I') +- Follow the Factory pattern for complex object creation. + +## Dependency Injection & Services + +- Use constructor dependency injection with null checks via ArgumentNullException +- Register services with appropriate lifetimes (Singleton, Scoped, Transient) +- Use Microsoft.Extensions.DependencyInjection patterns +- Implement service interfaces for testability + +## Resource Management & Localization + +- Use ResourceManager for localized messages and error strings +- Separate LogMessages and ErrorMessages resource files +- Access resources via `_resourceManager.GetString("MessageKey")` + +## Async/Await Patterns + +- Use async/await for all I/O operations and long-running tasks +- Return Task or Task from async methods +- Use ConfigureAwait(false) where appropriate +- Handle async exceptions properly + +## Testing Standards + +- Use MSTest framework with FluentAssertions for assertions +- Follow AAA pattern (Arrange, Act, Assert) +- Use Moq for mocking dependencies +- Test both success and failure scenarios +- Include null parameter validation tests + +## Configuration & Settings + +- Use strongly-typed configuration classes with data annotations +- Implement validation attributes (Required, NotEmptyOrWhitespace) +- Use IConfiguration binding for settings +- Support appsettings.json configuration files + +## Semantic Kernel & AI Integration + +- Use Microsoft.SemanticKernel for AI operations +- Implement proper kernel configuration and service registration +- Handle AI model settings (ChatCompletion, Embedding, etc.) +- Use structured output patterns for reliable AI responses + +## Error Handling & Logging + +- Use structured logging with Microsoft.Extensions.Logging +- Include scoped logging with meaningful context +- Throw specific exceptions with descriptive messages +- Use try-catch blocks for expected failure scenarios + +## Performance & Security + +- Use C# 12+ features and .NET 8 optimizations where applicable +- Implement proper input validation and sanitization +- Use parameterized queries for database operations +- Follow secure coding practices for AI/ML operations + +## Code Quality + +- Ensure SOLID principles compliance +- Avoid code duplication through base classes and utilities +- Use meaningful names that reflect domain concepts +- Keep methods focused and cohesive +- Implement proper disposal patterns for resources diff --git a/CommTester.slnx b/CommTester.slnx index e1134bc..2343ce5 100644 --- a/CommTester.slnx +++ b/CommTester.slnx @@ -1,5 +1,5 @@ - + diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..71de8e6 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ParentAvalonia/App.axaml b/ParentAvalonia/App.axaml new file mode 100644 index 0000000..f12478b --- /dev/null +++ b/ParentAvalonia/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/ParentAvalonia/App.axaml.cs b/ParentAvalonia/App.axaml.cs new file mode 100644 index 0000000..700daf0 --- /dev/null +++ b/ParentAvalonia/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace ParentAvalonia; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/ParentWpf/ChildSession.cs b/ParentAvalonia/ChildSession.cs similarity index 98% rename from ParentWpf/ChildSession.cs rename to ParentAvalonia/ChildSession.cs index d3601e4..baf0ec7 100644 --- a/ParentWpf/ChildSession.cs +++ b/ParentAvalonia/ChildSession.cs @@ -1,10 +1,9 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.IO.Pipes; -using System.Text.Json; using CommIpc; -namespace ParentWpf; +namespace ParentAvalonia; public sealed class ChildSession : NotifyBase, IAsyncDisposable { diff --git a/ParentWpf/MainViewModel.cs b/ParentAvalonia/MainViewModel.cs similarity index 87% rename from ParentWpf/MainViewModel.cs rename to ParentAvalonia/MainViewModel.cs index 7ebc31a..12ad2e3 100644 --- a/ParentWpf/MainViewModel.cs +++ b/ParentAvalonia/MainViewModel.cs @@ -2,11 +2,10 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.IO.Pipes; -using System.Text.Json; -using System.Windows; +using Avalonia.Threading; using CommIpc; -namespace ParentWpf; +namespace ParentAvalonia; public sealed class MainViewModel : NotifyBase, IAsyncDisposable { @@ -115,39 +114,28 @@ public sealed class MainViewModel : NotifyBase, IAsyncDisposable private static Process StartChildProcess(int childId, string pipeName) { - // Prefer spawning the built exe. If not found, fall back to running the dll via dotnet. + // Cross-platform friendly: prefer running the built dll via 'dotnet'. string? solutionRoot = TryFindSolutionRoot(AppContext.BaseDirectory); if (solutionRoot is null) { throw new InvalidOperationException("Could not find solution root (CommTester.slnx)."); } - string debugExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.exe"); - string releaseExe = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.exe"); string debugDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Debug", "net10.0", "ChildWorker.dll"); string releaseDll = Path.Combine(solutionRoot, "ChildWorker", "bin", "Release", "net10.0", "ChildWorker.dll"); + string childDll = File.Exists(debugDll) + ? debugDll + : File.Exists(releaseDll) + ? releaseDll + : string.Empty; + string fileName; string arguments; - if (File.Exists(debugExe)) - { - fileName = debugExe; - arguments = $"--pipe \"{pipeName}\" --id {childId}"; - } - else if (File.Exists(releaseExe)) - { - fileName = releaseExe; - arguments = $"--pipe \"{pipeName}\" --id {childId}"; - } - else if (File.Exists(debugDll)) + if (!string.IsNullOrWhiteSpace(childDll)) { fileName = "dotnet"; - arguments = $"\"{debugDll}\" --pipe \"{pipeName}\" --id {childId}"; - } - else if (File.Exists(releaseDll)) - { - fileName = "dotnet"; - arguments = $"\"{releaseDll}\" --pipe \"{pipeName}\" --id {childId}"; + arguments = $"\"{childDll}\" --pipe \"{pipeName}\" --id {childId}"; } else { @@ -197,10 +185,7 @@ public sealed class MainViewModel : NotifyBase, IAsyncDisposable break; } - await Application.Current.Dispatcher.InvokeAsync(() => - { - HandleIncomingFrame(session, frame); - }); + await Dispatcher.UIThread.InvokeAsync(() => HandleIncomingFrame(session, frame)); } } catch (OperationCanceledException) @@ -209,7 +194,7 @@ public sealed class MainViewModel : NotifyBase, IAsyncDisposable } catch (Exception ex) { - await Application.Current.Dispatcher.InvokeAsync(() => + await Dispatcher.UIThread.InvokeAsync(() => { session.Status = "Error"; session.AddLog($"[parent] Receive loop error: {ex.Message}"); @@ -217,7 +202,7 @@ public sealed class MainViewModel : NotifyBase, IAsyncDisposable } finally { - await Application.Current.Dispatcher.InvokeAsync(() => + await Dispatcher.UIThread.InvokeAsync(() => { session.IsConnected = false; session.Status = "Disconnected"; diff --git a/ParentAvalonia/MainWindow.axaml b/ParentAvalonia/MainWindow.axaml new file mode 100644 index 0000000..eae66dc --- /dev/null +++ b/ParentAvalonia/MainWindow.axaml @@ -0,0 +1,52 @@ + + + + +