From 3ebf7ce481ccd93b8c3dedd4df91e311c7109bec Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:36:11 -0400 Subject: [PATCH 01/10] Fix CA1000 / MA0003 / RCS1140 / RCS1163 in Result and Try MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Result.cs: - CA1000 (Do not declare static members on generic types): suppresses on Result with a justification — Failure / Success factories are central to the public API and consumers explicitly specify T at the call site by design. - MA0003 (Name the parameter): adds 'succeeded:' to 6 boolean literal arguments at constructor / factory call sites. Try.cs: - RCS1140 (Add exception to documentation comment): adds to the doc comments of Run, Run, RunAsync, and RunAsync so the existing throws are documented (4 doc blocks, covering 6 throw sites across NET5+ and pre-NET5 #if branches). - RCS1163 (Unused parameter 'token'): explicitly discards the token parameter in RunAsync with '_ = token;' since it can't be flowed into Func> / Func> (no token parameter on the delegate). Discard documents the intent. Currently surfaces as warnings under main but becomes Release errors once the centralized analyzer config in chore/template-drift-fixes lands. Landing this fix first keeps that PR's CI green. Release build clean: 0 warnings, 0 errors across all TFMs. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Wolfgang.TryPattern/Result.cs | 16 ++++++++++------ src/Wolfgang.TryPattern/Try.cs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Wolfgang.TryPattern/Result.cs b/src/Wolfgang.TryPattern/Result.cs index 9499b57..e631ea8 100644 --- a/src/Wolfgang.TryPattern/Result.cs +++ b/src/Wolfgang.TryPattern/Result.cs @@ -54,14 +54,14 @@ protected Result public static Result Failure(string errorMessage) => string.IsNullOrWhiteSpace(errorMessage) ? throw new ArgumentException("errorMessage cannot be empty", nameof(errorMessage)) - : new Result(false, errorMessage); + : new Result(succeeded: false, errorMessage); /// /// Creates a successful . /// - public static Result Success() => new(true, string.Empty); + public static Result Success() => new(succeeded: true, string.Empty); @@ -154,6 +154,10 @@ public static bool AllSucceeded(params Result[]? results) => /// property will contain a message as to why. If the operation succeeded the /// property will contain the return value from the function. /// +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1000:Do not declare static members on generic types", + Justification = "Result.Failure / Result.Success are factory methods central to the public API; consumers explicitly specify T at the call site by design.")] public class Result : Result { @@ -196,12 +200,12 @@ public class Result : Result public static new Result Failure(string errorMessage) => string.IsNullOrWhiteSpace(errorMessage) ? throw new ArgumentException("errorMessage cannot be empty", nameof(errorMessage)) - : new Result(false, errorMessage, default!); + : new Result(succeeded: false, errorMessage, default!); #else public static new Result Failure(string errorMessage) => string.IsNullOrWhiteSpace(errorMessage) ? throw new ArgumentException("errorMessage cannot be empty", nameof(errorMessage)) - : new Result(false, errorMessage, default!); + : new Result(succeeded: false, errorMessage, default!); #endif @@ -210,9 +214,9 @@ public class Result : Result /// Creates a successful with specified value. /// #if NET5_0_OR_GREATER - public static Result Success(T? value) => new(true, string.Empty, value); + public static Result Success(T? value) => new(succeeded: true, string.Empty, value); #else - public static Result Success(T value) => new(true, string.Empty, value); + public static Result Success(T value) => new(succeeded: true, string.Empty, value); #endif diff --git a/src/Wolfgang.TryPattern/Try.cs b/src/Wolfgang.TryPattern/Try.cs index 152c4c1..e80c7fe 100644 --- a/src/Wolfgang.TryPattern/Try.cs +++ b/src/Wolfgang.TryPattern/Try.cs @@ -18,6 +18,7 @@ public static class Try /// /// A that indicates if the action was successful. /// + /// is null. public static Result Run(Action action) { if (action == null) @@ -47,6 +48,7 @@ public static Result Run(Action action) /// A indicating if the function was successful or not and the result of /// the function if it was. /// + /// is null. #if NET5_0_OR_GREATER public static Result Run(Func function) { @@ -93,6 +95,7 @@ public static Result Run(Func? function) /// /// A of representing the asynchronous operation. /// + /// is null. public static async Task RunAsync(Action action, CancellationToken token = default) { if (action == null) @@ -126,6 +129,7 @@ public static async Task RunAsync(Action action, CancellationToken token /// /// A of representing the asynchronous operation. /// + /// is null. #if NET5_0_OR_GREATER public static async Task> RunAsync(Func> function, CancellationToken token = default) { @@ -134,6 +138,10 @@ public static async Task RunAsync(Action action, CancellationToken token throw new ArgumentNullException(nameof(function)); } + // The token parameter is part of the public API for call-site signalling but cannot be + // passed into Func> (no token parameter on the delegate). Discard it explicitly. + _ = token; + try { var result = await function().ConfigureAwait(false); @@ -156,6 +164,10 @@ public static async Task> RunAsync(Func> function, Cancella throw new ArgumentNullException(nameof(function)); } + // The token parameter is part of the public API for call-site signalling but cannot be + // passed into Func> (no token parameter on the delegate). Discard it explicitly. + _ = token; + try { var result = await function().ConfigureAwait(false); From cd8d767c8264b425f012dd8d03195e7ffe542532 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:37:08 -0400 Subject: [PATCH 02/10] Fix remaining analyzer violations surfaced after centralized rollout Resolves the code-side failures blocking PR #94's CI: - ResultTests.cs (4 sites): add 'succeeded:' named argument to bool literal in TestResult constructor (MA0003). - CSharp.DotNet8.Example/Program.cs: add 'content:' named argument to GetWordCount(null) (MA0003). - TryBenchmarks.cs: replace 'async () => await Task.CompletedTask' with sync no-op '() => { }' for the Action overload of RunAsync. The async lambda was being coerced to async-void (Action signature is 'void Action()'), which AsyncFixer03 + VSTHRD101 correctly flagged. The Action benchmarks should pass sync delegates. - VB.DotNet8.Example/Program.vb: wrap MainAsync().GetAwaiter(). GetResult() in #Disable Warning RS0030 / #Enable Warning since this is the standard VB sync entry-point pattern (no async Main in VB) and canonical's [examples/**/*.cs] glob doesn't match .vb. Note: the related .editorconfig sync (RS0030 = none in tests and benchmarks, picking up repo-template#329) is split into a separate PR because .editorconfig is a protected file. This PR depends on that one landing first to silence RS0030 violations on DateTime.Now in ResultOfTTests.cs and Task.Wait() in RunAsyncActionTests.cs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Wolfgang.TryPattern.Benchmarks/TryBenchmarks.cs | 8 ++------ examples/CSharp.DotNet8.Example/Program.cs | 2 +- examples/VB.DotNet8.Example/Program.vb | 2 ++ tests/Wolfgang.TryPattern.Tests/ResultTests.cs | 8 ++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/benchmarks/Wolfgang.TryPattern.Benchmarks/TryBenchmarks.cs b/benchmarks/Wolfgang.TryPattern.Benchmarks/TryBenchmarks.cs index 80b82a4..078929e 100644 --- a/benchmarks/Wolfgang.TryPattern.Benchmarks/TryBenchmarks.cs +++ b/benchmarks/Wolfgang.TryPattern.Benchmarks/TryBenchmarks.cs @@ -34,7 +34,7 @@ public async Task RunAsync_Action_Success() { for (var i = 0; i < OperationCount; i++) { - await Try.RunAsync(async () => await Task.CompletedTask); + await Try.RunAsync(() => { }); } } @@ -43,11 +43,7 @@ public async Task RunAsync_Action_WithException() { for (var i = 0; i < OperationCount; i++) { - await Try.RunAsync(async () => - { - await Task.CompletedTask; - throw new InvalidOperationException(); - }); + await Try.RunAsync(() => throw new InvalidOperationException()); } } diff --git a/examples/CSharp.DotNet8.Example/Program.cs b/examples/CSharp.DotNet8.Example/Program.cs index 733b2f0..298b92b 100644 --- a/examples/CSharp.DotNet8.Example/Program.cs +++ b/examples/CSharp.DotNet8.Example/Program.cs @@ -33,7 +33,7 @@ private static async Task Main() } // Try counting words in the file content - wordCountResult = Try.Run(() => GetWordCount(null)); + wordCountResult = Try.Run(() => GetWordCount(content: null)); // If word count failed, print the error message if (wordCountResult.Failed) diff --git a/examples/VB.DotNet8.Example/Program.vb b/examples/VB.DotNet8.Example/Program.vb index 0bb69fa..e76b790 100644 --- a/examples/VB.DotNet8.Example/Program.vb +++ b/examples/VB.DotNet8.Example/Program.vb @@ -7,7 +7,9 @@ Imports Wolfgang.TryPattern Friend Module Module1 Public Sub Main() +#Disable Warning RS0030 ' GetResult() is the standard VB sync entry-point pattern (no async Main support) MainAsync().GetAwaiter().GetResult() +#Enable Warning RS0030 End Sub Private Async Function MainAsync() As Task diff --git a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs index da329fd..6d599d2 100644 --- a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs @@ -10,7 +10,7 @@ private class TestResult(bool succeeded, string? errorMessage) : Result(succeede [Fact] public void Ctor_when_passed_true_and_empty_string_does_not_throw_Exception() { - var unused = new TestResult(true, string.Empty); + var unused = new TestResult(succeeded: true, string.Empty); } @@ -21,7 +21,7 @@ public void Ctor_when_passed_true_and_empty_string_does_not_throw_Exception() [InlineData("Test error")] public void Ctor_when_passed_true_and_non_empty_string_throw_InvalidOperationException(string? message) { - var ex = Assert.Throws(() => new TestResult(true, message)); + var ex = Assert.Throws(() => new TestResult(succeeded: true, message)); Assert.Equal("errorMessage", ex.ParamName); } @@ -30,7 +30,7 @@ public void Ctor_when_passed_true_and_non_empty_string_throw_InvalidOperationExc [Fact] public void Ctor_when_passed_false_and_message_does_not_throw_Exception() { - var unused = new TestResult(false, "Test error"); + var unused = new TestResult(succeeded: false, "Test error"); } @@ -41,7 +41,7 @@ public void Ctor_when_passed_false_and_message_does_not_throw_Exception() [InlineData("")] public void Ctor_when_passed_false_and_no_message_throw_InvalidOperationException(string? message) { - var ex = Assert.Throws(() => new TestResult(false, message)); + var ex = Assert.Throws(() => new TestResult(succeeded: false, message)); Assert.Equal("errorMessage", ex.ParamName); } From bd49338b8eeaef95a42e6031d407c87f33992c29 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 1 May 2026 12:28:04 -0400 Subject: [PATCH 03/10] RunAsync: observe cancellation token via ThrowIfCancellationRequested Replaces '_ = token;' (Roslynator RCS1163 fix that merely discards) with 'token.ThrowIfCancellationRequested();' so the parameter actually participates in cancellation. The delegate signature Func> has no token parameter so the token can't be flowed into the callback; the best we can do is fail fast if cancellation was already requested before invocation. The existing OperationCanceledException catch already rethrows this correctly. Also clarifies the doc to be honest about what "monitor" means here (checked before invocation, not flowed into the function) and adds a corresponding tag. Addresses Copilot review comments on PR #94 at Try.cs:144 and Try.cs:170. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Wolfgang.TryPattern/Try.cs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Wolfgang.TryPattern/Try.cs b/src/Wolfgang.TryPattern/Try.cs index e80c7fe..17469ef 100644 --- a/src/Wolfgang.TryPattern/Try.cs +++ b/src/Wolfgang.TryPattern/Try.cs @@ -125,11 +125,18 @@ public static async Task RunAsync(Action action, CancellationToken token /// /// The return type of the function. /// The function to execute. - /// The CancellationToken to monitor. + /// + /// A that is checked before is + /// invoked. If cancellation is already requested, an + /// is thrown without invoking . The token is not threaded into + /// itself; if you need cancellation inside the function, capture + /// the token in the lambda's closure. + /// /// /// A of representing the asynchronous operation. /// /// is null. + /// was already cancelled when this method was called. #if NET5_0_OR_GREATER public static async Task> RunAsync(Func> function, CancellationToken token = default) { @@ -138,9 +145,10 @@ public static async Task RunAsync(Action action, CancellationToken token throw new ArgumentNullException(nameof(function)); } - // The token parameter is part of the public API for call-site signalling but cannot be - // passed into Func> (no token parameter on the delegate). Discard it explicitly. - _ = token; + // Observe the token before invoking. The delegate signature has no token parameter, so we + // cannot flow it through; the best we can do is fail fast if cancellation was already + // requested. OperationCanceledException is rethrown by the catch below. + token.ThrowIfCancellationRequested(); try { @@ -164,9 +172,10 @@ public static async Task> RunAsync(Func> function, Cancella throw new ArgumentNullException(nameof(function)); } - // The token parameter is part of the public API for call-site signalling but cannot be - // passed into Func> (no token parameter on the delegate). Discard it explicitly. - _ = token; + // Observe the token before invoking. The delegate signature has no token parameter, so we + // cannot flow it through; the best we can do is fail fast if cancellation was already + // requested. OperationCanceledException is rethrown by the catch below. + token.ThrowIfCancellationRequested(); try { From ffd10e73d32a7458958ac234f0faa22a1f87695a Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 1 May 2026 20:01:03 -0400 Subject: [PATCH 04/10] Address Copilot review: spelling, comment accuracy, cancellation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 'cancelled' → 'canceled' on the OperationCanceledException XML doc for consistency with the .NET type name - Update misleading inline comment near token.ThrowIfCancellationRequested: it runs before the try/catch, not 'rethrown by the catch below'. The comment now correctly states the OCE propagates directly to the caller. - Add 2 tests covering the 'token already canceled' path: - RunAsync_Func_when_token_is_already_canceled_* - RunAsync_Func_nullable_when_token_is_already_canceled_* Both assert OperationCanceledException is thrown AND that the function delegate is not invoked, locking in the fail-fast semantics. Verified: dotnet build succeeds with 0 warnings, 97/97 tests pass on net10.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Wolfgang.TryPattern/Try.cs | 8 ++-- .../RunAsyncFuncTests.cs | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/Wolfgang.TryPattern/Try.cs b/src/Wolfgang.TryPattern/Try.cs index 17469ef..47155fd 100644 --- a/src/Wolfgang.TryPattern/Try.cs +++ b/src/Wolfgang.TryPattern/Try.cs @@ -136,7 +136,7 @@ public static async Task RunAsync(Action action, CancellationToken token /// A of representing the asynchronous operation. /// /// is null. - /// was already cancelled when this method was called. + /// was already canceled when this method was called. #if NET5_0_OR_GREATER public static async Task> RunAsync(Func> function, CancellationToken token = default) { @@ -147,7 +147,8 @@ public static async Task RunAsync(Action action, CancellationToken token // Observe the token before invoking. The delegate signature has no token parameter, so we // cannot flow it through; the best we can do is fail fast if cancellation was already - // requested. OperationCanceledException is rethrown by the catch below. + // requested. ThrowIfCancellationRequested propagates OperationCanceledException directly + // to the caller (it runs before the try/catch and is intentionally not wrapped in a Result). token.ThrowIfCancellationRequested(); try @@ -174,7 +175,8 @@ public static async Task> RunAsync(Func> function, Cancella // Observe the token before invoking. The delegate signature has no token parameter, so we // cannot flow it through; the best we can do is fail fast if cancellation was already - // requested. OperationCanceledException is rethrown by the catch below. + // requested. ThrowIfCancellationRequested propagates OperationCanceledException directly + // to the caller (it runs before the try/catch and is intentionally not wrapped in a Result). token.ThrowIfCancellationRequested(); try diff --git a/tests/Wolfgang.TryPattern.Tests/RunAsyncFuncTests.cs b/tests/Wolfgang.TryPattern.Tests/RunAsyncFuncTests.cs index b6e6205..e838536 100644 --- a/tests/Wolfgang.TryPattern.Tests/RunAsyncFuncTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/RunAsyncFuncTests.cs @@ -358,4 +358,46 @@ async Task Function() await Assert.ThrowsAsync(() => task); } + + + + [Fact] + public async Task RunAsync_Func_when_token_is_already_canceled_throws_OperationCanceledException_without_invoking_function() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var wasInvoked = false; + + Task Function() + { + wasInvoked = true; + return Task.FromResult(42); + } + + // Act & Assert + await Assert.ThrowsAsync(() => Try.RunAsync((Func>)Function, cts.Token)); + Assert.False(wasInvoked, "function should not be invoked when the token is already canceled"); + } + + + + [Fact] + public async Task RunAsync_Func_nullable_when_token_is_already_canceled_throws_OperationCanceledException_without_invoking_function() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var wasInvoked = false; + + Task Function() + { + wasInvoked = true; + return Task.FromResult(42); + } + + // Act & Assert + await Assert.ThrowsAsync(() => Try.RunAsync((Func>)Function, cts.Token)); + Assert.False(wasInvoked, "function should not be invoked when the token is already canceled"); + } } From 1b2852404c0da241ad833824c02081eb5828fbe0 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 11:35:56 -0400 Subject: [PATCH 05/10] Suppress noisy analyzer rules in test csproj; fix real test smells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows CI runner enforces a stricter analyzer set than local builds (.editorconfig [tests/**/*.cs] relaxations don't fully apply in CI). To make the test-project policy explicit and consistent, add a block to the test csproj covering: Style / test-pattern false positives: - VSTHRD200, VSTHRD003, VSTHRD103 (threading/async naming style) - AsyncFixer01 (single-await async methods are normal in tests) - MA0004, S6966 (ConfigureAwait & sync/async — tests need flexibility) - S2190 (false positive on loops that exit via thrown exception) - S2930 (CTS dispose — tests are short-lived; cts goes out of scope) Test-fixture exception illustration: - S3928 / MA0015 (ArgumentException without paramName — fixture style) - MA0012 (tests intentionally throw NullReferenceException to verify the Try wrapper catches/swallows it) Real test bugs fixed in code: - ResultTests.cs (S2699 / S1481): the two 'does_not_throw' tests now use Record.Exception(...) + Assert.Null instead of an unused local variable - RunFuncTests.cs (S4144): Run_Func_WithNullableStringFunction was byte-identical to Run_Func_WithStringFunction. Now actually exercises Func returning null and asserts result.Value is null. Verified: 0 warnings/errors local clean build, all 97 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Wolfgang.TryPattern.Tests/ResultTests.cs | 6 ++++-- .../Wolfgang.TryPattern.Tests/RunFuncTests.cs | 5 ++--- .../Wolfgang.TryPattern.Tests.Unit.csproj | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs index 6d599d2..0c10b86 100644 --- a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs @@ -10,7 +10,8 @@ private class TestResult(bool succeeded, string? errorMessage) : Result(succeede [Fact] public void Ctor_when_passed_true_and_empty_string_does_not_throw_Exception() { - var unused = new TestResult(succeeded: true, string.Empty); + var ex = Record.Exception(() => new TestResult(succeeded: true, string.Empty)); + Assert.Null(ex); } @@ -30,7 +31,8 @@ public void Ctor_when_passed_true_and_non_empty_string_throw_InvalidOperationExc [Fact] public void Ctor_when_passed_false_and_message_does_not_throw_Exception() { - var unused = new TestResult(succeeded: false, "Test error"); + var ex = Record.Exception(() => new TestResult(succeeded: false, "Test error")); + Assert.Null(ex); } diff --git a/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs b/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs index 296c48a..e59bf34 100644 --- a/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs @@ -78,8 +78,7 @@ public void Run_Func_WithStringFunction_ReturnsResult() public void Run_Func_WithNullableStringFunction_ReturnsResult() { // Arrange - const string expectedValue = "Hello, World!"; - static string Function() => expectedValue; + static string? Function() => null; // Act var result = Try.Run(Function); @@ -88,7 +87,7 @@ public void Run_Func_WithNullableStringFunction_ReturnsResult() Assert.True(result.Succeeded); Assert.False(result.Failed); Assert.Empty(result.ErrorMessage!); - Assert.Equal(expectedValue, result.Value); + Assert.Null(result.Value); } diff --git a/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj b/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj index de2ddfe..d6be098 100644 --- a/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj +++ b/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj @@ -10,6 +10,25 @@ true Copyright 2025 Chris Wolfgang + + $(NoWarn); + VSTHRD200; + VSTHRD003; + VSTHRD103; + AsyncFixer01; + MA0004; + S6966; + S2190; + S2930; + S3928; + MA0015; + MA0012 + From f9e609d70475a84aaa22346ebb46e4ee78f9ccc0 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 12:27:06 -0400 Subject: [PATCH 06/10] Restore Run_Func_WithNullableStringFunction value assertion Per review: the test name refers to the function *signature* being nullable (Func), not the return *value* being null. The function should still return a real string and the test should assert on it. This keeps the original "Hello, World!" coverage while fixing the S4144 duplicate-method complaint by using Func instead of Func. The other test (Run_Func_WithStringFunction_ReturnsResult) tests the non-nullable Func signature; this one tests Func. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs b/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs index e59bf34..db8584b 100644 --- a/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/RunFuncTests.cs @@ -78,7 +78,8 @@ public void Run_Func_WithStringFunction_ReturnsResult() public void Run_Func_WithNullableStringFunction_ReturnsResult() { // Arrange - static string? Function() => null; + const string expectedValue = "Hello, World!"; + static string? Function() => expectedValue; // Act var result = Try.Run(Function); @@ -87,7 +88,7 @@ public void Run_Func_WithNullableStringFunction_ReturnsResult() Assert.True(result.Succeeded); Assert.False(result.Failed); Assert.Empty(result.ErrorMessage!); - Assert.Null(result.Value); + Assert.Equal(expectedValue, result.Value); } From e63db1a72ad406e032bb4fbd24ccf000ea15efbb Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 12:30:12 -0400 Subject: [PATCH 07/10] Restore original 'does not throw' test pattern; suppress S2699/S1481 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: revert the Record.Exception rewrites in ResultTests.cs. The original 'var unused = new TestResult(...)' tests are valid xUnit 'does not throw' patterns — xUnit reports unhandled exceptions during test execution as failures, so an explicit assertion is not required. Add S2699 (no-assertion) and S1481 (unused-local) to the test csproj NoWarn block to satisfy the analyzers without changing the test code. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/Wolfgang.TryPattern.Tests/ResultTests.cs | 6 ++---- .../Wolfgang.TryPattern.Tests.Unit.csproj | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs index 0c10b86..6d599d2 100644 --- a/tests/Wolfgang.TryPattern.Tests/ResultTests.cs +++ b/tests/Wolfgang.TryPattern.Tests/ResultTests.cs @@ -10,8 +10,7 @@ private class TestResult(bool succeeded, string? errorMessage) : Result(succeede [Fact] public void Ctor_when_passed_true_and_empty_string_does_not_throw_Exception() { - var ex = Record.Exception(() => new TestResult(succeeded: true, string.Empty)); - Assert.Null(ex); + var unused = new TestResult(succeeded: true, string.Empty); } @@ -31,8 +30,7 @@ public void Ctor_when_passed_true_and_non_empty_string_throw_InvalidOperationExc [Fact] public void Ctor_when_passed_false_and_message_does_not_throw_Exception() { - var ex = Record.Exception(() => new TestResult(succeeded: false, "Test error")); - Assert.Null(ex); + var unused = new TestResult(succeeded: false, "Test error"); } diff --git a/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj b/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj index d6be098..1252ea4 100644 --- a/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj +++ b/tests/Wolfgang.TryPattern.Tests/Wolfgang.TryPattern.Tests.Unit.csproj @@ -27,7 +27,9 @@ S2930; S3928; MA0015; - MA0012 + MA0012; + S2699; + S1481 From fc46885bfc6a97baf0fc21fcf7433c99b373d0f9 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 12:56:43 -0400 Subject: [PATCH 08/10] Document OperationCanceledException on both RunAsync overloads Address Copilot review: - RunAsync(Action, CancellationToken): the token flows into Task.Run and cancellation is explicitly rethrown via 'catch (OperationCanceled) throw;'. The XML docs only mentioned ArgumentNullException, which could surprise consumers. Add an entry for OCE and expand the doc to describe both fail-paths (before-start and during-execution). - RunAsync(Func>, CancellationToken): the existing token doc only described the 'already canceled' fail-fast. But the catch also rethrows OCE if 'function' observes cancellation during execution (e.g. await Task.Delay(_, token) inside the lambda). Expand both and docs to cover both paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Wolfgang.TryPattern/Try.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Wolfgang.TryPattern/Try.cs b/src/Wolfgang.TryPattern/Try.cs index 47155fd..e4f5dd7 100644 --- a/src/Wolfgang.TryPattern/Try.cs +++ b/src/Wolfgang.TryPattern/Try.cs @@ -91,11 +91,17 @@ public static Result Run(Func? function) /// Executes the specified action asynchronously, catching any exception that may occur. /// /// The action to execute. - /// The CancellationToken to monitor. + /// + /// A that is passed to . + /// If cancellation is observed (either before starts or while it runs), + /// an is propagated to the caller rather than wrapped + /// in a failed . + /// /// /// A of representing the asynchronous operation. /// /// is null. + /// Cancellation was observed via . public static async Task RunAsync(Action action, CancellationToken token = default) { if (action == null) @@ -130,13 +136,20 @@ public static async Task RunAsync(Action action, CancellationToken token /// invoked. If cancellation is already requested, an /// is thrown without invoking . The token is not threaded into /// itself; if you need cancellation inside the function, capture - /// the token in the lambda's closure. + /// the token in the lambda's closure. If observes the token during + /// execution (e.g. via ) and the resulting + /// escapes, it is also propagated to the caller + /// rather than wrapped in a failed . /// /// /// A of representing the asynchronous operation. /// /// is null. - /// was already canceled when this method was called. + /// + /// was already canceled when this method was called, or + /// observed cancellation during execution and let an + /// escape. + /// #if NET5_0_OR_GREATER public static async Task> RunAsync(Func> function, CancellationToken token = default) { From ab6f92ba70e4ee4d6efd74f17a64926a266391f8 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 13:06:27 -0400 Subject: [PATCH 09/10] Suppress noisy analyzer rules in benchmarks and examples csproj CI surfaced remaining analyzer errors in benchmarks and examples that my earlier fix only addressed in the test csproj. Same root cause: the Windows CI runner doesn't fully honor .editorconfig section relaxations for [benchmarks/**/*.cs] and [examples/**/*.cs], so we move the suppressions to the project level where MSBuild applies them reliably. Benchmarks NoWarn: MA0004, VSTHRD200, S6966 Examples NoWarn: MA0004, S6966 Verified: 0 warnings/errors local clean build. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Wolfgang.TryPattern.Benchmarks.csproj | 12 ++++++++++++ .../CSharp.DotNet8.Example.csproj | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/benchmarks/Wolfgang.TryPattern.Benchmarks/Wolfgang.TryPattern.Benchmarks.csproj b/benchmarks/Wolfgang.TryPattern.Benchmarks/Wolfgang.TryPattern.Benchmarks.csproj index d41901f..b86d1eb 100644 --- a/benchmarks/Wolfgang.TryPattern.Benchmarks/Wolfgang.TryPattern.Benchmarks.csproj +++ b/benchmarks/Wolfgang.TryPattern.Benchmarks/Wolfgang.TryPattern.Benchmarks.csproj @@ -5,6 +5,18 @@ net8.0 enable enable + + + $(NoWarn); + MA0004; + VSTHRD200; + S6966 + diff --git a/examples/CSharp.DotNet8.Example/CSharp.DotNet8.Example.csproj b/examples/CSharp.DotNet8.Example/CSharp.DotNet8.Example.csproj index 8bb7afe..94aa9a3 100644 --- a/examples/CSharp.DotNet8.Example/CSharp.DotNet8.Example.csproj +++ b/examples/CSharp.DotNet8.Example/CSharp.DotNet8.Example.csproj @@ -5,6 +5,17 @@ net8.0 enable enable + + + $(NoWarn); + MA0004; + S6966 + From 431629e3b3014daa6e263db73c7103bc3d98d778 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Sat, 2 May 2026 13:53:45 -0400 Subject: [PATCH 10/10] Clarify Task.Run cancellation semantics in RunAsync(Action) docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Copilot review: Task.Run(action, token) only honors cancellation *before* the action is scheduled — once the synchronous action starts running, the token can't interrupt it. Cancellation during execution only happens if the action itself cooperatively checks the token. Updated and the entry to spell this out so callers don't expect Task.Run to forcibly cancel running work. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Wolfgang.TryPattern/Try.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Wolfgang.TryPattern/Try.cs b/src/Wolfgang.TryPattern/Try.cs index e4f5dd7..b62d17c 100644 --- a/src/Wolfgang.TryPattern/Try.cs +++ b/src/Wolfgang.TryPattern/Try.cs @@ -93,15 +93,22 @@ public static Result Run(Func? function) /// The action to execute. /// /// A that is passed to . - /// If cancellation is observed (either before starts or while it runs), - /// an is propagated to the caller rather than wrapped - /// in a failed . + /// If cancellation is requested before begins executing, the task is + /// canceled and an is propagated to the caller rather + /// than wrapped in a failed . Once has started, + /// cannot interrupt it; cancellation during + /// execution is only observed if itself cooperatively checks the + /// token (e.g. via ) and throws. /// /// /// A of representing the asynchronous operation. /// /// is null. - /// Cancellation was observed via . + /// + /// Cancellation was observed via (either before + /// started, or because itself observed + /// the token and threw). + /// public static async Task RunAsync(Action action, CancellationToken token = default) { if (action == null)