Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bbf9845
Bump library TFMs to net10.0/net11.0; mark AOT-compatible
buvinghausen May 12, 2026
9930e81
Drop redundant Microsoft.SourceLink.GitHub package reference
buvinghausen May 12, 2026
b958267
Add net11.0 to the test TFM matrix
buvinghausen May 12, 2026
1c7786a
Enable Runtime Async for net11.0 test target
buvinghausen May 12, 2026
c2b0418
Add empty AOT smoke-test project
buvinghausen May 12, 2026
e59caaf
Exercise all generator codepaths in the AOT smoke-test
buvinghausen May 12, 2026
5c3fd47
CI: install .NET 11 SDK and run AOT smoke-test publish
buvinghausen May 12, 2026
3ae6d97
Release workflow: pack with the .NET 11 SDK
buvinghausen May 12, 2026
10c2cc3
README: document .NET 10 / 11 / AOT / Runtime Async support
buvinghausen May 12, 2026
08febcd
CLAUDE.md: correct generator-only layout and add net10/11+AOT context
buvinghausen May 12, 2026
4bf7aa7
Post-review hygiene: explicit SelfContained + --no-restore on AOT pub…
buvinghausen May 12, 2026
d5d3522
Adapt editorconfig to .NET 11 latest-Recommended ruleset
buvinghausen May 13, 2026
4b1df4d
Speed up GHA
buvinghausen May 13, 2026
d642c3e
Add AotSmokeTest project to solution file
buvinghausen May 13, 2026
714ec0d
Light up C# syntax highlighting in generator raw-string literals
buvinghausen May 13, 2026
bf00787
Restore net8/net9 test matrix coverage in tests and CI setup
Copilot May 13, 2026
8c94654
Dump it back out to .net8
buvinghausen May 13, 2026
184b10c
Use version-based MSBuild condition for IsAotCompatible
Copilot May 13, 2026
0271251
Apply latest review feedback on TFM condition and README
Copilot May 13, 2026
a5aa361
Fix docs
buvinghausen May 13, 2026
5a03a42
Merge branch 'feature/net11-aot' of https://github.com/buvinghausen/T…
buvinghausen May 13, 2026
5fc6699
AOT came w/ .NET 7
buvinghausen May 13, 2026
529cda4
Smoke 8
buvinghausen May 13, 2026
932328f
FIX CI
buvinghausen May 13, 2026
87136a2
last fix
buvinghausen May 13, 2026
fc1559b
Address remaining review threads on TFMs, AOT condition, and docs
Copilot May 13, 2026
1759265
Remove editorconfig exclusions
buvinghausen May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ indent_style = tab
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true

# Omit accessibility modifiers when they match the language default (e.g. private on class members)
dotnet_style_require_accessibility_modifiers = omit_if_default:error

# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:error
dotnet_style_qualification_for_property = false:error
Expand All @@ -42,6 +45,12 @@ dotnet_style_explicit_tuple_names = true:error

# CSharp code style settings:
[*.cs]
# Prefer file-scoped namespace declarations (single `namespace Foo;`)
csharp_style_namespace_declarations = file_scoped:error

# Allow single-statement bodies without braces (e.g. `if (x) F();`, `for (...) F();`)
csharp_prefer_braces = false:silent

# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:error
csharp_style_var_when_type_is_apparent = true:error
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
8.0.*
9.0.*
10.0.*
11.0.*
env:
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
Expand All @@ -25,3 +26,7 @@ jobs:
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build -v n /p:CollectCoverage=true /p:CoverletOutput='../../' /p:CoverletOutputFormat=opencover /p:Threshold=91 /p:SkipAutoProps=true /p:UseSourceLink=true
- name: AOT smoke-test (net8.0)
run: dotnet publish test/TaskTupleAwaiter.AotSmokeTest -c Release -f net8.0 --no-restore
- name: AOT smoke-test (net11.0)
run: dotnet publish test/TaskTupleAwaiter.AotSmokeTest -c Release -f net11.0 --no-restore
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.*
dotnet-version: 11.0.*
env:
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
Expand Down
30 changes: 18 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

## Project Overview

TaskTupleAwaiter provides extension methods that allow you to `await` a `ValueTuple` of `Task<T>` (or non-generic `Task`) instances and destructure the results in a single line. The `source_generator` branch replaces the hand-authored `TaskTupleExtensions.cs` (up to arity 16) with a Roslyn incremental source generator.
TaskTupleAwaiter provides extension methods that allow you to `await` a `ValueTuple` of `Task<T>` (or non-generic `Task`) instances and destructure the results in a single line. In this repository, a Roslyn incremental source generator (`src/TaskTupleAwaiter.Generator`) generates the extension-method source during library build under `namespace System.Threading.Tasks`, and that generated code is compiled into `TaskTupleAwaiter.dll` for each target framework. Consumers install and reference the compiled package binaries; the generator is a private build-time implementation detail.

## Repository Layout

```
TaskTupleAwaiter/
├── src/
│ ├── TaskTupleAwaiter/ # Main library (netstandard2.0)
│ │ └── TaskTupleExtensions.cs # Hand-authored fallback / reference implementation
│ ├── TaskTupleAwaiter/ # Main library shell (netstandard2.0, net462, net8.0)
│ │ # No hand-authored .cs sources — code is generated at build and compiled into the library.
│ └── TaskTupleAwaiter.Generator/ # Roslyn incremental source generator (netstandard2.0)
│ └── TaskTupleExtensionsGenerator.cs
├── test/
│ └── TaskTupleAwaiter.Tests/ # xUnit test project
│ ├── TaskTupleAwaiterTests.cs
│ ├── BehaviorComparisonTests.cs
│ ├── Adapters/
│ │ └── AwaiterAdapter.cs
│ ├── DummyException.cs
│ ├── On.cs
│ └── SpySynchronizationContext.cs
│ ├── TaskTupleAwaiter.Tests/ # xUnit v3 test project
│ │ ├── TaskTupleAwaiterTests.cs
│ │ ├── BehaviorComparisonTests.cs
│ │ ├── Adapters/
│ │ │ └── AwaiterAdapter.cs
│ │ ├── DummyException.cs
│ │ ├── On.cs
│ │ └── SpySynchronizationContext.cs
│ └── TaskTupleAwaiter.AotSmokeTest/ # NativeAOT downstream-consumer smoke-test (net8.0, net11.0)
│ ├── TaskTupleAwaiter.AotSmokeTest.csproj
│ └── Program.cs
├── docs/superpowers/ # Specs and implementation plans
├── README.md
├── LICENSE.txt
└── CLAUDE.md # This file
Expand All @@ -32,7 +36,9 @@ TaskTupleAwaiter/
| Concern | Choice |
|---|---|
| Language | C# 14.0 |
| Library / Generator target | .NET Standard 2.0 |
| Library TFMs | netstandard2.0, net462, net8.0 |
| Generator target | netstandard2.0 (Roslyn analyzer requirement) |
| AOT-compatible TFMs | net8.0+ (`<IsAotCompatible>true</IsAotCompatible>` via `IsTargetFrameworkCompatible`) |
| Generator framework | Roslyn `IIncrementalGenerator` |
| Test framework | xUnit v3 |
| Assertion library | Shouldly |
Expand Down
5 changes: 3 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project>
<PropertyGroup>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ImplicitUsings>true</ImplicitUsings>
<LangVersion>latest</LangVersion>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (user, orders) = await (GetUserAsync(id), GetOrdersAsync(id));
- **Non-generic `Task` support** — await tuples of `Task` (not just `Task<T>`) when you don't need return values
- **Zero dependencies** — a single file, no external packages (except `System.ValueTuple` on .NET Framework 4.6.2)
Comment thread
buvinghausen marked this conversation as resolved.
- **Broad compatibility** — targets .NET Standard 2.0, .NET Framework 4.6.2, and .NET 8+
- **NativeAOT ready** — the package sets `<IsAotCompatible>true</IsAotCompatible>` for .NET 8+ targets, and CI publishes downstream NativeAOT smoke tests

## Installation

Expand Down
1 change: 1 addition & 0 deletions TaskTupleAwaiter.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
<Project Path="test\TaskTupleAwaiter.Tests\TaskTupleAwaiter.Tests.csproj">
<Configuration Solution="Release|*" Project="*|*|NoBuild" />
</Project>
<Project Path="test\TaskTupleAwaiter.AotSmokeTest\TaskTupleAwaiter.AotSmokeTest.csproj" />
</Solution>
47 changes: 23 additions & 24 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
<Project>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" Condition="'' != $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" Condition="'' != $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net8.0</TargetFrameworks>
<Authors>Joseph Musser,Brian Buvinghausen</Authors>
<Company>Buvinghausen Solutions</Company>
<Copyright>Copyright © $([System.DateTime]::UtcNow.ToString("yyyy")) Brian Buvinghausen</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/buvinghausen/TaskTupleAwaiter/blob/master/README.md</PackageProjectUrl>
<PackageTags>C# 7.0;Value Tuple;Async Await Elegant Code</PackageTags>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
<PackageOutputPath>..\..\</PackageOutputPath>
</PropertyGroup>
<PropertyGroup>
<Authors>Joseph Musser,Brian Buvinghausen</Authors>
<Company>Buvinghausen Solutions</Company>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
<Copyright>Copyright © $([System.DateTime]::UtcNow.ToString("yyyy")) Brian Buvinghausen</Copyright>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageOutputPath>..\..\</PackageOutputPath>
<PackageProjectUrl>https://github.com/buvinghausen/TaskTupleAwaiter/blob/master/README.md</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>C# 7.0;Value Tuple;Async Await Elegant Code</PackageTags>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TargetFrameworks>netstandard2.0;net462;net8.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="*" PrivateAssets="All" />
<None Include="../../LICENSE.txt" Pack="true" PackagePath="$(PackageLicenseFile)" />
<None Include="../../README.md" Pack="true" PackagePath="$(PackageReadmeFile)" />
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>
<ItemGroup>
<None Include="../../LICENSE.txt" Pack="true" PackagePath="$(PackageLicenseFile)" />
<None Include="../../README.md" Pack="true" PackagePath="$(PackageReadmeFile)" />
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions src/TaskTupleAwaiter.Generator/CSharpEmit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace TaskTupleAwaiter.Generator;

static class CSharpEmit
{
// Identical to StringBuilder.AppendLine at runtime; the [StringSyntax("C#")] annotation
// is what Visual Studio 2022 and JetBrains Rider use to syntax-highlight the raw-string
// content at each call site as C# instead of as opaque text.
public static StringBuilder AppendCSharp(this StringBuilder sb, [StringSyntax("C#")] string code) =>
sb.AppendLine(code);
}
13 changes: 13 additions & 0 deletions src/TaskTupleAwaiter.Generator/StringSyntaxAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Polyfill for System.Diagnostics.CodeAnalysis.StringSyntaxAttribute (added in .NET 7).
// The generator project targets netstandard2.0, so the BCL definition isn't available.
// Roslyn's IDE classifiers recognise the attribute by namespace + type name (not assembly
// identity), so an internal declaration here drives the embedded-language hint in VS / Rider.
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace System.Diagnostics.CodeAnalysis;
#pragma warning restore IDE0130 // Namespace does not match folder structure

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)]
sealed class StringSyntaxAttribute(string syntax) : Attribute
{
public string Syntax { get; } = syntax;
}
22 changes: 11 additions & 11 deletions src/TaskTupleAwaiter.Generator/TaskTupleAwaiter.Generator.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>false</IsPackable>
<IsRoslynComponent>true</IsRoslynComponent>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="*" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="*" PrivateAssets="all" />
</ItemGroup>

</Project>
30 changes: 15 additions & 15 deletions src/TaskTupleAwaiter.Generator/TaskTupleExtensionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ static string GenerateSource(bool hasAwaitOptions)
{
StringBuilder sb = new();

sb.AppendLine(
sb.AppendCSharp(
"""
// <auto-generated/>
using System.Runtime.CompilerServices;
Expand All @@ -55,15 +55,15 @@ public static class TaskTupleExtensions
AppendTypedArity(sb, arity, hasAwaitOptions);
AppendNonGenericSection(sb, hasAwaitOptions);

sb.AppendLine("}");
sb.AppendCSharp("}");
return sb.ToString();
}

// ── Typed Task<T> ─────────────────────────────────────────────────────

static void AppendTypedArity1(StringBuilder sb, bool hasAwaitOptions)
{
sb.AppendLine(
sb.AppendCSharp(
"""
#region (Task<T1>)

Expand All @@ -78,15 +78,15 @@ public static ConfiguredTaskAwaitable<T1> ConfigureAwait<T1>(this ValueTuple<Tas
""");

if (hasAwaitOptions)
sb.AppendLine(
sb.AppendCSharp(
"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public static ConfiguredTaskAwaitable<T1> ConfigureAwait<T1>(this ValueTuple<Task<T1>> tasks, ConfigureAwaitOptions options) =>
tasks.Item1.ConfigureAwait(options);

""");

sb.AppendLine(
sb.AppendCSharp(
"""
#endregion

Expand All @@ -102,7 +102,7 @@ static void AppendTypedArity(StringBuilder sb, int arity, bool hasAwaitOptions)
? "continueOnCapturedContext ? ConfigureAwaitOptions.ContinueOnCapturedContext : ConfigureAwaitOptions.None"
: "continueOnCapturedContext";

sb.AppendLine(
sb.AppendCSharp(
$"""
#region (Task<T1>..Task<T{arity}>)

Expand All @@ -117,7 +117,7 @@ static void AppendTypedArity(StringBuilder sb, int arity, bool hasAwaitOptions)
""");

if (hasAwaitOptions)
sb.AppendLine(
sb.AppendCSharp(
$"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public static TupleConfiguredTaskAwaitable<{tp}> ConfigureAwait<{tp}>(this {tupleType} tasks, ConfigureAwaitOptions options) =>
Expand All @@ -129,14 +129,14 @@ static void AppendTypedArity(StringBuilder sb, int arity, bool hasAwaitOptions)

AppendTupleConfiguredTaskAwaitableStruct(sb, arity, tp, tupleType, optionsType);

sb.AppendLine(
sb.AppendCSharp(
"""
#endregion

""");
}

static void AppendTupleTaskAwaiterStruct(StringBuilder sb, int arity, string tp, string tupleType) => sb.AppendLine(
static void AppendTupleTaskAwaiterStruct(StringBuilder sb, int arity, string tp, string tupleType) => sb.AppendCSharp(
$$"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public readonly record struct TupleTaskAwaiter<{{tp}}> : ICriticalNotifyCompletion
Expand Down Expand Up @@ -174,7 +174,7 @@ public void UnsafeOnCompleted(Action continuation) =>
""");

static void AppendTupleConfiguredTaskAwaitableStruct(StringBuilder sb, int arity, string tp,
string tupleType, string optionsType) => sb.AppendLine(
string tupleType, string optionsType) => sb.AppendCSharp(
$$"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public readonly record struct TupleConfiguredTaskAwaitable<{{tp}}>
Expand Down Expand Up @@ -232,7 +232,7 @@ public void UnsafeOnCompleted(Action continuation) =>

static void AppendNonGenericSection(StringBuilder sb, bool hasAwaitOptions)
{
sb.AppendLine(
sb.AppendCSharp(
"""
#region Task

Expand All @@ -247,7 +247,7 @@ public static ConfiguredTaskAwaitable ConfigureAwait(this ValueTuple<Task> tasks
""");

if (hasAwaitOptions)
sb.AppendLine(
sb.AppendCSharp(
"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public static ConfiguredTaskAwaitable ConfigureAwait(this ValueTuple<Task> tasks, ConfigureAwaitOptions options) =>
Expand All @@ -260,7 +260,7 @@ public static ConfiguredTaskAwaitable ConfigureAwait(this ValueTuple<Task> tasks
AppendNonGenericArity(sb, arity, hasAwaitOptions);
}

sb.AppendLine(
sb.AppendCSharp(
"""
#endregion

Expand All @@ -271,7 +271,7 @@ static void AppendNonGenericArity(StringBuilder sb, int arity, bool hasAwaitOpti
{
var tupleType = NonGenericTaskTupleType(arity);

sb.AppendLine(
sb.AppendCSharp(
$"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public static TaskAwaiter GetAwaiter(this {tupleType} tasks) =>
Expand All @@ -285,7 +285,7 @@ public static ConfiguredTaskAwaitable ConfigureAwait(this {tupleType} tasks, boo

if (!hasAwaitOptions) return;

sb.AppendLine(
sb.AppendCSharp(
$"""
/// <summary>This type and its members are intended for use by the compiler.</summary>
public static ConfiguredTaskAwaitable ConfigureAwait(this {tupleType} tasks, ConfigureAwaitOptions options) =>
Expand Down
12 changes: 7 additions & 5 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" Condition="'' != $([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<!-- .NET 8 & 9 end on November 10, 2026 -->
<TargetFrameworks>net10.0;net9.0;net8.0;net472</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591;IDE0051</NoWarn>
<OutputType>Exe</OutputType>
<TargetFrameworks>net11.0;net10.0;net9.0;net8.0;net472</TargetFrameworks>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>

<ItemGroup>
<Using Include="Shouldly" />
<Using Include="Xunit" />
<PackageReference Include="Shouldly" Version="4.*" />
<PackageReference Include="xunit.v3.mtp-v2" Version="3.*" />
<Using Include="Shouldly" />
<Using Include="Xunit" />
<Using Remove="System.IO" />
<Using Remove="System.Net.Http" />
</ItemGroup>

</Project>
Loading
Loading