This file is the Living Knowledge Ledger for the Universal Device Toolkit Plugins project. Every time an AI agent or human developer solves a complex bug, discovers a Windows OS quirk, or optimizes an architecture, they MUST append a structured entry here.
### [YYYY-MM-DD] <Brief Description>
- **Symptom / Pitfall**: What failed?
- **Root Cause**: Why did it fail?
- **Enforced Rule**: What mandatory constraint prevents recursion?
- **.NET/OS Version**: Under what environment was this learned?- Symptom / Pitfall: Multiple Roslyn analyzer warnings (CA1062, CA2024, CS1591) accumulating in build output
- Root Cause:
- CA1062: Missing null validation in public methods
- CA2024:
EndOfStreamused in async context (should useReadLineAsync) - CS1591: Missing XML documentation for public APIs
- Enforced Rule:
- Added
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>toDirectory.Build.props - Added
<WarningsAsErrors>CS1591;CA1062;CA2024</WarningsAsErrors> - All public methods MUST have
ArgumentNullException.ThrowIfNull() - All async methods MUST NOT use
EndOfStream - All public APIs MUST have XML documentation (
/// <summary>)
- Added
- .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall: CI validation failure:
plugin.json version does not match plugin.manifest.json version - Root Cause:
plugin.jsonhad version1.1.9whileplugin.manifest.jsonhad1.2.0. Manual version updates cause drift. - Enforced Rule:
plugin.manifest.jsonis the SINGLE SOURCE OF TRUTH for versionplugin.jsonMUST be auto-generated fromplugin.manifest.json(or strictly synced).csproj<Version>/<FileVersion>/<AssemblyVersion>MUST matchplugin.manifest.jsonversion[Plugin(... version: "...")]in*Plugin.csMUST matchplugin.manifest.jsonversion- Use
.\llt-plugin.cmd bump-version --plugin <id> --part patch|minor|majorto bump, orsync-versionto propagate without bumping promoteonly prepares store metadata (store-entry.json); it does not bump versions
- .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall: Compilation error:
CS0103: The name 'ArgumentNullException' does not exist in the current context - Root Cause: Added
ArgumentNullException.ThrowIfNull()calls but forgot to addusing System;to the file - Enforced Rule:
- ALL
.csfiles MUST have explicitusing System;if they useArgumentNullException,NullReferenceException, etc. - When adding null validation to a file, ALWAYS check that
using System;is present - Prefer full-qualified names (
System.ArgumentNullException.ThrowIfNull()) if modifying legacy code withoutusing System;
- ALL
- .NET/OS Version: .NET 10, C# 13 (preview)
- Symptom / Pitfall: Fallback UI (when WPF fails to load XAML) shows white background in Dark theme
- Root Cause:
WpfFallbackHelper.csused hardcodedBrushes.White,Brushes.Black,Brushes.Gray - Enforced Rule:
- ALL fallback UI MUST use
DynamicResourceor theme-aware brush resolution - Created
ResolveFallbackBrush(string lightBrush, string darkBrush)method - Never use
Brushes.*directly in fallback UI code - Test fallback UI in BOTH Light and Dark themes
- ALL fallback UI MUST use
- .NET/OS Version: .NET 10, Windows 11 24H2, WPF UI 4.3.0
- Symptom / Pitfall:
CA2024: Use 'ReadLineAsync' instead of 'EndOfStream' in async method - Root Cause:
StreamReader.EndOfStreamis a synchronous property that blocks in async context - Enforced Rule:
- In async methods, NEVER use
EndOfStream - Use
ReadLineAsync()in a loop until it returnsnull - Example fix:
string? line; while ((line = await streamReader.ReadLineAsync()) != null) { // Process line }
- In async methods, NEVER use
- .NET/OS Version: .NET 10, C# 13 (preview)
- NEVER use
ConfigureAwait(false)in WPF projects — it strips the UI synchronization context - Always use
Dispatcher.CheckAccess()andDispatcher.InvokeAsync()for background-triggered UI updates
- ALL WMI queries MUST use async wrappers with 2,500ms–3,000ms timeouts
- ALL administrative process executions (
netsh,sc) MUST useCancellationToken - Test WMI queries under Remote Desktop sessions (they behave differently!)
- High-frequency monitoring loops (500ms–2000ms) MUST NOT serialize JSON on every tick
- Use in-memory caching and only write to disk every 5–10 seconds
- NEVER use
Debug.WriteLine()in production polling loops (it kills performance)
- ALL UI elements MUST use
CornerRadius="8"orCornerRadius="10"(rounded cards) - ALL colors MUST use
DynamicResource(e.g.,{DynamicResource ControlFillColorDefaultBrush}) - NEVER write monolithic 600-line StackPanels — use
Gridstar-sizing andWrapPanel - ALL text MUST use
TextWrapping="Wrap"andTextTrimming="CharacterEllipsis"
- ALL user-facing strings MUST be in
Resource.resx(never hardcode English text) - Use numbered placeholders (
{0},{1}) — NEVER string concatenation - Generate satellite assemblies for all 34 languages in
<PluginSatelliteResourceLanguages>
When evaluating UI via FlaUI + WinRT OCR:
- Untranslated Detection: Flag English text in non-English locales
- Mojibake & Encoding Corruption: Flag corrupted UTF-8/16 characters
- Broken Placeholders: Flag unreplaced
{0}or raw binding tags - Layout Truncation & Box Overflow: Compare OCR text width vs. container width
- Technical Domain Semantics: Verify accurate hardware terminology
- ALL plugins MUST pass
.\llt-plugin.cmd validate --profile contributor - Version consistency check:
plugin.json=plugin.manifest.json=.csproj <Version> - Build output MUST have 0 warnings, 0 errors (enforced via
TreatWarningsAsErrors)
store-entry.jsonMUST byte-match thestoreblock of the siblingplugin.manifest.json(promote/generate-sync keeps them in lockstep).- NEVER regenerate the root
store.jsonwholesale viagenerate-storewhen some plugins lack a published release ZIP: it zeroesfileSize/hashfor those plugins (silent regression). Merge thestore-entry.jsonof a new plugin incrementally instead. - Release assets are the single source for
fileSize/hash; only presentrelease-assets/<id>-v<ver>.zipentries may carry nonzero sizes.
- Use
.\llt-plugin.cmd promoteto generatestore-entry.json - Release ZIP naming convention:
<plugin-id>-v<version>.zip - Tag format:
v<version>-<plugin-id>(e.g.,v1.2.0-network-acceleration)
- Timestamp / Version: 2026-07-14 Phase 3 hard cutover, .NET 10, host vendored as UniversalDeviceToolkit.Lib v5.0.0
- History: Prior to Phase 3, internal compile identifiers stayed
LenovoLegionToolkit.*for host ABI (BUGS.md M-010). Host and plugins now shareUniversalDeviceToolkit.*assembly/namespace identity. - Enforced Rule (Phase 3): Primary code, assemblies, namespaces,
x:Class, manifestclass, solution/csproj names, and SDK/Shared outputs useUniversalDeviceToolkit.*. Brand user-visible text as "Universal Device Toolkit". - Legacy path retention (do NOT "fix"):
SettingsManager<T>, ShellIntegration profile/lang migration still read pre-rebrand data under%LocalAppData%\LenovoLegionToolkit\...and migrate into%LocalAppData%\UniversalDeviceToolkit\.... Those path segment strings are intentional migration sources. - Evidence (Phase 3): Shared + SDK + BatteryHealth Release build 0/0; Shared.Tests 210/210 pass; outputs
UniversalDeviceToolkit.Plugins.SDK.dll/UniversalDeviceToolkit.Plugins.Shared.dll.
- Main repo
UniversalDeviceToolkit.Lib/UniversalDeviceToolkit.Lib.Pluginsship AssemblyName/RootNamespaceUniversalDeviceToolkit.Lib/UniversalDeviceToolkit.Lib.Plugins. Plugin repo compile identifiers match. M-010 ABI rename gate is closed for new builds (old LLT-named plugin ZIPs require host dual-load or recompile).
- Timestamp / Version: 2026-07-06, .NET 10, WPF plugin UI
- Symptom / Pitfall:
ViveToolPage.xaml.csawaited service calls with.ConfigureAwait(false)(15 sites), stripping the capturedDispatcherSynchronizationContext. No live race existed (the page marshals UI touches viaawait Dispatcher.InvokeAsync(...)), but drifting from the repo thread-safety contract risked regressions. - Root Cause: UI code-behind copied a Lib/SDK-style await hint.
.ConfigureAwait(false)is only safe in non-UI/library code where noSynchronizationContextcapture is wanted. - Enforced Rule: Zero
.ConfigureAwait(false)in**/*.xaml.cs. UI code-behind relies on the capturedDispatcherSynchronizationContext(omit the hint, or use.ConfigureAwait(true)for an explicit guarantee). Only Lib/SDK/non-UI code may use.ConfigureAwait(false). Enforce via a CI grep gate (rg -g '*.xaml.cs' 'ConfigureAwait\(false\)'=> 0). - Evidence: PLG-001 remediated in working tree (concurrent editor removed the 15 offending sites; sole remaining await hint is
ConfigureAwait(true)at L829, UI-safe); 0ConfigureAwait(false)across all plugin*.xaml.cs; full solution build 0 warnings/0 errors; 409/409 tests green.
- Timestamp / Version: 2026-07-06, .NET 10, xUnit, Windows 11 24H2
- Symptom / Pitfall:
BatteryHealthPluginTests.Plugin_HasExpectedMetadatafailed intermittently withExpected: 电池健康vsActual: Battery Healthonly under the full-solution parallel test run; the plugin tests passed in isolation (16/16). - Root Cause:
LocalizedTextTestsBase.TextClass_HasNoHardcodedChinesemutates the process-wide staticResources.Resource.Culturetoenvia reflection (restored infinally). xUnit runs each plugin's*TextTestsconcurrently with its*PluginTests. The metadata assertion reads the culture-dependentplugin.NameandXText.PluginNamein two separate operations; a parallel text test flips the static fromnulltoenbetween the reads, so the two reads disagree. Latent in CustomMouse / NetworkAcceleration / ShellIntegration (same tautological two-readAssert.Equalpattern); ViveTool immune (usesIsNullOrWhiteSpace+ already serialises its test collection). - Enforced Rule: Never assert culture-dependent properties with a tautological two-read pattern while a sibling test mutates the shared static culture. Pin test classes that share a mutable static into one
[CollectionDefinition("<Name>", DisableParallelization = true)]and tag both the writer (TextTests) and reader (PluginTests) classes with[Collection("<Name>")]. Mirror per plugin whenever a*TextTestsbase mutates a process-wide static.
- Timestamp / Version: 2026-07-06, .NET 10, WPF plugin UI, Windows 11 24H2
- Symptom / Pitfall: Plugin settings pages (
ViveToolSettingsPage,ShellIntegrationStyleSettingsWindow) surfaced errors/confirmations via synchronous modalMessageBox.Show(6 sites in ViveTool, 1 in ShellIntegration). Modal dialogs block the host WPF message pump, steal focus from the host window, and break the embedded-plugin non-blocking UX contract. - Root Cause: Plugin code-behind copied a WinForms-style modal-feedback pattern. The repo UI-governance rule demands inline, non-blocking status feedback inside a hosted plugin surface (no modal focus steal).
- Enforced Rule: Never use
MessageBox.Showin plugin UI code-behind. Surface status inline via a statusTextBlock+ aSetStatus(string, bool)/ShowInlineStatushelper, themed viaDynamicResourceforeground by error/success state (no modal focus steal).WpfHostNotifications.csis the sole allowed modal site (host-level fallback notifications). Enforce via a CI grep gate:rg -g '*.cs' 'MessageBox\.Show' Plugins=> onlyWpfHostNotifications.cs. - Evidence: 6
MessageBox.Showsites inViveToolSettingsPage.xaml.csreplaced with_statusTextBlock+SetStatus(text, isError)(themed foreground,SymbolIconglyph,AutomationProperties.AutomationId = "ViveToolSettingsStatusText"); 1 site inShellIntegrationStyleSettingsWindow.cs:135replaced with inline_statusTextBlock+ status method (AutomationId = "ShellIntegrationStyleSettingsStatusText").rg -n 'MessageBox\.Show' Plugins -g '*.cs'=> 0 in plugin code-behind (2 inWpfHostNotifications.cshost shim, allowed); full solution build 0 warnings/0 errors; 409/409 tests green (CustomMouse 54, ShellIntegration 114, BatteryHealth 16, NetworkAcceleration 39, ViveTool 186).
- Timestamp / Version: 2026-07-06, .NET 10, WPF plugin runtime, Windows 11 24H2
- Symptom / Pitfall:
BatteryHealthService.QueryFirstenumeratedManagementObjectSearcher.Get()synchronously. The outerGetBatteryHealthReportAsyncwrapped the body inTask.Run+cts.CancelAfter(WmiTimeoutMs=3000)andQueryFirstcheckedcancellationToken.ThrowIfCancellationRequested()per row, butManagementObjectSearcher.Get()is a blocking native COM enumeration that does NOT poll the CancellationToken. The per-row guard only runs AFTER a row is returned, so a hung ACPI/WMI provider pinned the thread-pool task (and the await of the caller) indefinitely past the 3,000ms contract. - Root Cause:
cts.CancelAfteris purely decorative for a hung native COM call once the delegate is already running; it cannot interrupt the blocking enumeration. The existing rule "ALL WMI queries MUST use async wrappers with 2,500ms-3,000ms timeouts" was satisfied nominally but NOT enforced as a HARD deadline - cancellation is cooperative, native COM does not cooperate. - Enforced Rule: For WMI queries behind a cancellation budget, do NOT rely on
CancelAfteralone. Race the blockingManagementObjectSearcher.Get()enumeration off-thread (Task.Run) against a hard deadline and ABANDON the task if it does not complete:Task.Wait(TimeSpan.FromMilliseconds(timeoutMs), cts.Token)returning false ->cts.Cancel()+ throwTimeoutException. This matches the host-sideManagementObjectSearcherExtensions.GetAsync(Task.WhenAny(task, Task.Delay(timeoutMs))+ abandon). When the enumeration faults inside the bounded task,Task.WaitthrowsAggregateException-> unwrap (ae.InnerExceptions.Count == 1 ? ae.InnerExceptions[0] : ae) to rethrow the originalManagementException/COMException/OperationCanceledExceptionso the typed catch blocks of the caller still match. Mirror this abandon pattern in any plugin WMI site that currently relies onCancelAfterfor a hung-provider deadline. - Evidence: PLG-004 remediated in working tree;
dotnet build UniversalDeviceToolkit-Plugins.sln -c Release=> 0 warnings / 0 errors;dotnet test Plugins\BatteryHealth.Tests --no-build=> 16/16 pass. Per-query hard timeoutWmiQueryTimeoutMs = 2500added (outerWmiTimeoutMs = 3000retained as the total budget ceiling).
- Timestamp / Version: 2026-07-14 (Phase 3), .NET 10
- Enforced Rule: (1) User-visible product framing MUST use "Universal Device Toolkit" / "通用设备工具包" / universal OEM scope — never Lenovo-Legion-exclusive positioning. (2) Community:
r/Lenovois fine;r/LenovoLegionis not. Competitor comparisons (e.g. "Lenovo Vantage") and historical GitHub topics may remain as factual references. (3) Compile identifiers (namespaces, assemblies, solution/csproj, manifestclass) are nowUniversalDeviceToolkit.*after Phase 3 hard cutover.
- Timestamp / Version: 2026-07-14, .NET 10
- Symptom / Pitfall: After LenovoLegionToolkit → UniversalDeviceToolkit rebrand, a contributor might "fix" hard-coded legacy path segments, breaking migration for pre-rebrand users.
- Root Cause: Both repos preserve the OLD name
LenovoLegionToolkitDELIBERATELY as a forward-migration source, while writing NEW data underUniversalDeviceToolkit. - Enforced Rule: (1) Active write roots MUST use
UniversalDeviceToolkit(%LocalAppData%\UniversalDeviceToolkit\plugins\<id>). (2)LenovoLegionToolkitpath segments are read-ONLY legacy-migration sources — DO NOT delete or rename them. (3) Prefer hostAppIdentity.CompactName/LegacyCompactNameover hard-coded literals where available. - Evidence: Plugin
SettingsManager<T>writes underUniversalDeviceToolkit\plugins\; retainsLenovoLegionToolkit\pluginsas legacy migration read source.
This file is alive. Every bug fix or architecture optimization MUST be recorded here. 📝
- Symptom / Pitfall: After upgrading vendored host DLLs from v3.6.14 to v4.2.1, plugin tests fail with
System.IO.FileNotFoundException: Could not load file or assembly 'Serilog, Version=4.3.0.0'. 15/409 tests fail (14 in ViveTool, 1 in NetworkAcceleration). PluginLoader-based workbench fails to load any plugin withInvalidOperationExceptionatPluginWorkbenchSession.cs:155. - Root Cause: v4.2.1
UniversalDeviceToolkit.Lib.dlluses Serilog 4.3.0 inLog..ctor()(logging initialization), which is a new transitive dependency not present in v3.6.14. The plugin repo'sDependencies\Host\only vendored the 3 host DLLs (Lib, Lib.Plugins, WPF) but not their transitive dependencies. - Enforced Rule:
Dependencies\Host\MUST contain all transitive dependencies of the vendored host DLLs, not just the top-level assemblies- When syncing host DLLs from the main repo's
Build\directory, copy ALL*.dllfiles (or at minimum, the transitive deps reachable viaAssembly.GetReferencedAssemblies()) ensure-host-dependencies.ps1andrefresh-host-references.ps1$requiredFileslists MUST be kept in sync with actual host transitive depshost-release.jsonMUST documenttransitiveDependenciesso the next sync doesn't drop them againDirectory.Build.targetsMUST copy host deps (including transitive) to test/tool output via a dedicatedCopyHostDependenciesToOutputtarget (plugin projects usePrivate=falsereferences and should NOT include them in plugin ZIPs)
- Detection Method: After any host DLL version bump, run
dotnet testandPluginWorkbench.Smokefor at least 1 plugin — Serilog/init failures surface immediately asFileNotFoundExceptionin the test host process. - .NET/OS Version: .NET 10, Windows 11 24H2, Serilog 4.3.0 + Sinks.Async 2.1.0 + Sinks.File 7.0.0
- Symptom / Pitfall: After adding
<Reference Include="UniversalDeviceToolkit.Lib.Plugins">toTools\PluginWorkbench\PluginWorkbench.csproj, build fails with 2 CS0104 errors:PluginHostModeis ambiguous betweenUniversalDeviceToolkit.Lib.Plugins.PluginHostModeandUniversalDeviceToolkit.Plugins.SDK.PluginHostMode. After adding the alias, 3 more CS0104 errors forPluginHostContext(static class). - Root Cause: The host Lib.Plugins DLL and the SDK both define the same types (intentional type forwarding / dual-surface design). The
using UniversalDeviceToolkit.Lib.Plugins;import was already present but harmless (the assembly was not referenced, so the types were invisible). Once Lib.Plugins.dll v4.2.1 was referenced, both namespaces became visible and C# disambiguation failed. - Enforced Rule:
- When adding the
UniversalDeviceToolkit.Lib.Pluginsreference to any project that also importsUniversalDeviceToolkit.Plugins.SDK, add using aliases for ALL shared types:using PluginHostMode = UniversalDeviceToolkit.Plugins.SDK.PluginHostMode;andusing PluginHostContext = UniversalDeviceToolkit.Plugins.SDK.PluginHostContext; - The SDK types are the correct consumer-facing types: SDK
PluginHostContext.Currenthas a public setter (forPluginHostContext.Current = _hostContextassignment), LibPluginHostContext.Currentis read-only (must useSetCurrent()method instead) — code that does property assignment only compiles against SDK - The intersection of types in both namespaces is:
IAppStartupPlugin,IOptimizationCategoryProvider,IPluginHostContext,IPluginPage,PluginHostContext,PluginHostMode— audit any newusingblock that imports both namespaces and alias all 6 if used unqualified
- When adding the
- .NET/OS Version: .NET 10, UniversalDeviceToolkit host v4.2.1
- Symptom / Pitfall:
ApplySettingsAsync_ReplacesCurrentSettingstest fails withIOException(file locked by another process) when all 5 plugin test assemblies run in parallel viadotnet testfull solution run. Passes 100% when run solo. - Root Cause: The host library
PluginConfiguration.SaveAsync()writes to a shared temp config path resolved byUniversalDeviceToolkit.Lib.Plugins.PluginBase. Cross-assembly parallel xUnit execution causes file contention between test host processes on this shared path. - Enforced Rule:
- All plugin
SaveSettingsAsync()methods that callConfiguration.SaveAsync()MUST wrap the call in a retry-with-backoff pattern (3 attempts, exponential 50ms delays) - Catch only
IOException(file contention) — do not retry on other exceptions - The root fix should be in the host library
PluginConfigurationusing per-process temp paths, but the retry pattern is a safe defensive fallback
- All plugin
- .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall: BatteryHealth plugin used author 'EliuaK_Csy' while all other 4 plugins used 'SSC-STUDIO'; NetworkAcceleration C# Plugin attribute said version '1.1.9' while manifest said '1.2.0'.
- Root Cause: BatteryHealth was contributed by a different author before repository standardization; NetworkAcceleration version was bumped in manifest without syncing the C# attribute.
- Enforced Rule:
- ALL plugins MUST use the same author value in plugin.json, plugin.manifest.json, and [Plugin(...)] attribute
- The plugin.manifest.json version is the SINGLE SOURCE OF TRUTH
- C# [Plugin(...)] attribute version MUST match plugin.manifest.json version
- After any manifest version bump, update C# attribute, plugin.json, and Build directory in the same commit
- .NET/OS Version: .NET 10, Windows 11 24H2
- History: Prior gate froze
LenovoLegionToolkit.*compile identifiers until host DLL rename. - Phase 3 (2026-07-14): Hard cutover complete — namespaces/assemblies/csproj/solution/manifest class fields are
UniversalDeviceToolkit.*. Still do not rename SettingsManager/ShellIntegration legacy%LocalAppData%\LenovoLegionToolkit\migration path strings. - .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall: WPF async-void event handlers (e.g. �sync void XxxButton_Click) perform async I/O (plugin loading, file dialogs, process bootstrap) without try-catch. Any unhandled exception propagates to the SynchronizationContext and tears down the whole host WPF process, since async-void cannot be observed by the caller.
- Root Cause: async-void methods do not surface exceptions to the caller; unhandled exceptions reach AppDomain.UnhandledException / TaskScheduler.UnobservedTaskException and crash. PluginWorkbench had 8 unguarded async-void handlers (MainWindow_Loaded, PluginListBox_MouseDoubleClick, LoadSelectedButton_Click, BootstrapHostButton_Click, OpenZipButton_Click, ReloadCurrentButton_Click, ModeToggleButton_Click, ModeComboBox_SelectionChanged).
- Enforced Rule (PLG-012):
- Every �sync void WPF event handler MUST wrap its body in ry { ... } catch (Exception ex) { <log + inline status> }.
- Log via a single AppendLog/PluginLog.Trace sink and surface the failure inline (e.g. StatusTextBlock.Text = "..."); never let exceptions escape async-void.
- Preserve state-cleanup side effects outside or in inally (e.g. _suppressModeSelectionChanged = false, button IsEnabled restore in BootstrapHostButton_Click).
- Control-flow early-returns (suppress-flag checks) should sit above the try or be returned from inside; the guard must not swallow expected cancellations.
- Reference pattern: RunOptimizationActionButton_Click (MainWindow.xaml.cs).
- .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall:
ViveToolDownloadService.DownloadViveToolAsync()callshttpClient.GetAsync()withHttpCompletionOption.ResponseHeadersReadbut stores the result in a plain local (var response). WhenEnsureSuccessStatusCode()throws (non-2xx status), theHttpResponseMessageis never disposed, and the underlying TCP connection is not returned to the pool — a socket leak that eventually exhausts the connection pool under repeated failures. - Root Cause: The
using var responsepattern was used consistently in the siblingImportFeaturesFromUrlAsync(line 253) but omitted inDownloadViveToolAsync(line 80). TheResponseHeadersReadoption makes this worse because the content stream has not been fully consumed, so the response cannot be implicitly cleaned up by the GC finalizer in a timely manner. - Enforced Rule:
- EVERY
HttpClient.GetAsync/PostAsync/SendAsynccall MUST useusing var response = await ...regardless of whether the content is consumed synchronously or asynchronously. - This rule applies even when the response stream is read and disposed separately via a nested
using— theresponseitself must still be covered byusingto guarantee cleanup on the exception path. - Reference:
ImportFeaturesFromUrlAsync(ViveToolDownloadService.cs) — the correct pattern. The buggy pattern wasvar response = await ...; response.EnsureSuccessStatusCode();withoutusing.
- EVERY
- .NET/OS Version: .NET 10, Windows 11 24H2
- Symptom / Pitfall: Plugin SDK
PluginHostContext.ResolveType()andCreateHostWindow()both contained barecatch { return null; }blocks that swallowed every exception — includingFileLoadException(assembly version mismatch),BadImageFormatException(corrupt DLL),SecurityException, andTargetInvocationException(constructor body threw). When the host couldn't resolve a plugin's host bridge or settings window, the entire system silently degraded to "Preview mode" with zero diagnostic output. - Root Cause: Code assumed any assembly/host-window resolution failure was benign and returned null silently. In practice
FileLoadException(version policy failure),SecurityException, andBadImageFormatExceptionare genuine diagnostic issues that developers need visibility into. - Enforced Rule:
- Catch and return null ONLY for expected failures (
FileNotFoundException,BadImageFormatExceptionfor assemblies;MissingMethodExceptionfor missing constructors). - For unexpected exceptions,
Debug.WriteLine(...)surfaces the failure in Debug builds (zero-cost in Release). This is the minimum diagnostic requirement for SDK/library code. - Never use bare
catch { }/catch { return null; }in plugin SDK code — always catch a specific type or at minimumcatch (Exception ex)with diagnostic output. - Reference:
PluginHostContext.ResolveTypeandCreateHostWindow.
- Catch and return null ONLY for expected failures (
- .NET/OS Version: .NET 10, Windows 11 24H2