From 71f897cf97339754adf5471000ad41e282874039 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 27 Aug 2026 15:58:47 -0600 Subject: [PATCH 1/2] Exclude non-equivalent async API alternatives Ship default VSTHRD103 exclusions for xUnit assertions, NUnit assertions, and Entity Framework Core APIs whose async variants have different semantics or specialized use cases. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docfx/analyzers/VSTHRD103.md | 1 - docfx/analyzers/configuration.md | 13 +++ ...thodsToExcludeFromVSTHRD103.frameworks.txt | 8 ++ .../VSTHRD103UseAsyncOptionAnalyzerTests.cs | 81 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.frameworks.txt diff --git a/docfx/analyzers/VSTHRD103.md b/docfx/analyzers/VSTHRD103.md index 120310cf3..d278c24c1 100644 --- a/docfx/analyzers/VSTHRD103.md +++ b/docfx/analyzers/VSTHRD103.md @@ -35,4 +35,3 @@ Some APIs may have async versions that are less efficient or inappropriate for c See our [configuration](configuration.md) topic to learn how to exclude specific methods using the `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` file. -``` diff --git a/docfx/analyzers/configuration.md b/docfx/analyzers/configuration.md index 13b3eede3..75e868f7d 100644 --- a/docfx/analyzers/configuration.md +++ b/docfx/analyzers/configuration.md @@ -105,6 +105,19 @@ excluded from VSTHRD103 analysis by specifying them in a configuration file. **Generic sample:** ``[Microsoft.EntityFrameworkCore.DbSet`1]::Add`` +The analyzer package supplies default exclusions for APIs whose Async-suffixed alternatives +are intended for different operations or exceptional use cases: + +| API | Excluded methods | +| --- | --- | +| Entity Framework Core `DbContext` | `Add`, `AddRange` | +| Entity Framework Core `DbSet` | `Add`, `AddRange` | +| NUnit `Assert` | `That` | +| xUnit `Assert` | `Throws`, `ThrowsAny` | + +Projects and NuGet packages can extend this list by adding their own files that match the +filename pattern above. + ## Additional synchronous blocking methods for VSTHRD002 Projects that wrap synchronous waits in their own APIs can configure those methods to be diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.frameworks.txt b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.frameworks.txt new file mode 100644 index 000000000..14b536b01 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/buildTransitive/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.frameworks.txt @@ -0,0 +1,8 @@ +# These APIs have Async-suffixed alternatives that are not generally equivalent to or preferred over their synchronous counterparts. +[Microsoft.EntityFrameworkCore.DbContext]::Add +[Microsoft.EntityFrameworkCore.DbContext]::AddRange +[Microsoft.EntityFrameworkCore.DbSet`1]::Add +[Microsoft.EntityFrameworkCore.DbSet`1]::AddRange +[NUnit.Framework.Assert]::That +[Xunit.Assert]::Throws +[Xunit.Assert]::ThrowsAny diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs index 677b716f6..e98729969 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs @@ -2649,6 +2649,87 @@ public static void SlowSyncMethod() { } await CSVerify.VerifyAnalyzerAsync(test); } + [Fact] + public async Task KnownFrameworkMethodsWithNonEquivalentAsyncAlternativesDoNotGenerateWarning() + { + string test = """ + using System; + using System.Threading.Tasks; + + class Test + { + async Task TestAsync() + { + Xunit.Assert.Throws(() => { }); + Xunit.Assert.ThrowsAny(() => { }); + NUnit.Framework.Assert.That(true, true); + + var context = new Microsoft.EntityFrameworkCore.DbContext(); + context.Add(new object()); + context.AddRange(new object()); + + var set = new Microsoft.EntityFrameworkCore.DbSet(); + set.Add(new object()); + set.AddRange(new object()); + + Unrelated.Assert.{|#0:Throws|}(() => { }); + } + } + + namespace Xunit + { + static class Assert + { + public static void Throws(Action action) { } + public static Task ThrowsAsync(Action action) => Task.CompletedTask; + public static void ThrowsAny(Action action) { } + public static Task ThrowsAnyAsync(Action action) => Task.CompletedTask; + } + } + + namespace NUnit.Framework + { + static class Assert + { + public static void That(T actual, T expected) { } + public static Task ThatAsync(T actual, T expected) => Task.CompletedTask; + } + } + + namespace Microsoft.EntityFrameworkCore + { + class DbContext + { + public void Add(object entity) { } + public Task AddAsync(object entity) => Task.CompletedTask; + public void AddRange(params object[] entities) { } + public Task AddRangeAsync(params object[] entities) => Task.CompletedTask; + } + + class DbSet + { + public void Add(T entity) { } + public Task AddAsync(T entity) => Task.CompletedTask; + public void AddRange(params T[] entities) { } + public Task AddRangeAsync(params T[] entities) => Task.CompletedTask; + } + } + + namespace Unrelated + { + static class Assert + { + public static void Throws(Action action) { } + public static Task ThrowsAsync(Action action) => Task.CompletedTask; + } + } + """; + + await CSVerify.VerifyAnalyzerAsync( + test, + CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("Throws", "ThrowsAsync")); + } + [Fact] public async Task GenericTypeExclusion_DoesNotExcludeNonGenericType() { From 8261d14658697120796ef68733863a254c803931 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 27 Aug 2026 16:05:19 -0600 Subject: [PATCH 2/2] Clarify VSTHRD103 exclusion filenames Document the optional suffix accepted for AdditionalFiles that contribute VSTHRD103 exclusions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docfx/analyzers/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docfx/analyzers/configuration.md b/docfx/analyzers/configuration.md index 75e868f7d..d2c22a3b9 100644 --- a/docfx/analyzers/configuration.md +++ b/docfx/analyzers/configuration.md @@ -97,7 +97,8 @@ when in an async context. Sometimes certain APIs have async versions but those a are significantly slower, less efficient, or simply not preferred. These methods can be excluded from VSTHRD103 analysis by specifying them in a configuration file. -**Filename:** `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` +**Filename:** `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt`, or +`vs-threading.SyncMethodsToExcludeFromVSTHRD103..txt` **Line format:** `[Namespace.TypeName]::MethodName`