diff --git a/Build.ps1 b/Build.ps1
index 94a0fa4..5b6ab1d 100644
--- a/Build.ps1
+++ b/Build.ps1
@@ -16,30 +16,6 @@ Process {
$outputDir = Join-Path $PSScriptRoot ".output"
$nupkgsPath = Join-Path $outputDir "*.nupkg"
- $testProjectName = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName())
- $testProjectDir = Join-Path $outputDir $testProjectName
- $testProjectPath = Join-Path $testProjectDir "$testProjectName.csproj"
- $testNugetConfigPath = Join-Path $testProjectDir "nuget.config"
- $testNugetConfigContents = @"
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"@
-
try {
Push-Location $workingDir
Remove-Item $outputDir -Force -Recurse -ErrorAction SilentlyContinue
@@ -54,14 +30,8 @@ Process {
# Pack using NuGet.exe
Exec { & nuget pack Workleap.DotNet.CodingStandards.nuspec -OutputDirectory $outputDir -Version $version -ForceEnglishOutput }
- # Create a new test console project, add our newly created package and try to build it in release mode
- # The default .NET console project template with top-level statements should not trigger any warnings
- # We treat warnings as errors even though it's supposed to be already enabled by our package,
- # just in case the package is not working as expected
- Exec { & dotnet new console --name $testProjectName --output $testProjectDir }
- Set-Content -Path $testNugetConfigPath -Value $testNugetConfigContents
- Exec { & dotnet add $testProjectPath package Workleap.DotNet.CodingStandards --version $version }
- Exec { & dotnet build $testProjectPath --configuration Release /p:TreatWarningsAsErrors=true }
+ # Run tests
+ Exec { & dotnet test --configuration Release --logger "console;verbosity=detailed" }
# Push to a NuGet feed if the environment variables are set
if (($null -ne $env:NUGET_SOURCE ) -and ($null -ne $env:NUGET_API_KEY)) {
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..9bf91b0
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+ enable
+ enable
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/build/Workleap.DotNet.CodingStandards.props b/src/build/Workleap.DotNet.CodingStandards.props
new file mode 100644
index 0000000..a144399
--- /dev/null
+++ b/src/build/Workleap.DotNet.CodingStandards.props
@@ -0,0 +1,29 @@
+
+
+ true
+ strict
+ true
+ true
+ latest-all
+ true
+
+
+ true
+
+
+ true
+ true
+ true
+ true
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
diff --git a/src/build/Workleap.DotNet.CodingStandards.targets b/src/build/Workleap.DotNet.CodingStandards.targets
new file mode 100644
index 0000000..583b67e
--- /dev/null
+++ b/src/build/Workleap.DotNet.CodingStandards.targets
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
diff --git a/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.props b/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.props
new file mode 100644
index 0000000..bea1969
--- /dev/null
+++ b/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.props
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.targets b/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.targets
new file mode 100644
index 0000000..57f9970
--- /dev/null
+++ b/src/buildMultiTargeting/Workleap.DotNet.CodingStandards.targets
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/buildTransitive/Workleap.DotNet.CodingStandards.props b/src/buildTransitive/Workleap.DotNet.CodingStandards.props
index a144399..bea1969 100644
--- a/src/buildTransitive/Workleap.DotNet.CodingStandards.props
+++ b/src/buildTransitive/Workleap.DotNet.CodingStandards.props
@@ -1,29 +1,3 @@
-
- true
- strict
- true
- true
- latest-all
- true
-
-
- true
-
-
- true
- true
- true
- true
- true
-
-
- true
-
-
- true
-
-
- true
-
-
+
+
\ No newline at end of file
diff --git a/src/buildTransitive/Workleap.DotNet.CodingStandards.targets b/src/buildTransitive/Workleap.DotNet.CodingStandards.targets
index 583b67e..57f9970 100644
--- a/src/buildTransitive/Workleap.DotNet.CodingStandards.targets
+++ b/src/buildTransitive/Workleap.DotNet.CodingStandards.targets
@@ -1,31 +1,3 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/CodingStandardTests.cs b/tests/Workleap.DotNet.CodingStandards.Tests/CodingStandardTests.cs
new file mode 100644
index 0000000..42e3252
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/CodingStandardTests.cs
@@ -0,0 +1,66 @@
+using Workleap.DotNet.CodingStandards.Tests.Helpers;
+using Xunit.Abstractions;
+
+namespace Workleap.DotNet.CodingStandards.Tests;
+
+public sealed class CodingStandardTests(PackageFixture fixture, ITestOutputHelper testOutputHelper) : IClassFixture
+{
+ [Fact]
+ public async Task BannedSymbolsAreReported()
+ {
+ using var project = new ProjectBuilder(fixture, testOutputHelper);
+ project.AddCsprojFile();
+ project.AddFile("sample.cs", "_ = System.DateTime.Now;");
+ var data = await project.BuildAndGetOutput();
+ Assert.True(data.HasWarning("RS0030"));
+ }
+
+ [Fact]
+ public async Task WarningsAsErrorOnGitHubActions()
+ {
+ using var project = new ProjectBuilder(fixture, testOutputHelper);
+ project.AddCsprojFile();
+ project.AddFile("sample.cs", "_ = System.DateTime.Now;");
+ var data = await project.BuildAndGetOutput(["--configuration", "Release", "/p:GITHUB_ACTIONS=true"]);
+ Assert.True(data.HasError("RS0030"));
+ }
+
+ [Fact]
+ public async Task NamingConvention_Invalid()
+ {
+ using var project = new ProjectBuilder(fixture, testOutputHelper);
+ project.AddCsprojFile();
+ project.AddFile("sample.cs", """
+ _ = "";
+
+ class Sample
+ {
+ private readonly int field;
+
+ public Sample(int a) => field = a;
+
+ public int A() => field;
+ }
+ """);
+ var data = await project.BuildAndGetOutput(["--configuration", "Release"]);
+ Assert.True(data.HasError("IDE1006"));
+ }
+
+ [Fact]
+ public async Task NamingConvention_Valid()
+ {
+ using var project = new ProjectBuilder(fixture, testOutputHelper);
+ project.AddCsprojFile();
+ project.AddFile("sample.cs", """
+ _ = "";
+
+ class Sample
+ {
+ private int _field;
+ }
+ """);
+ var data = await project.BuildAndGetOutput(["--configuration", "Release"]);
+ Assert.False(data.HasError("IDE1006"));
+ Assert.False(data.HasWarning("IDE1006"));
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/PathHelpers.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/PathHelpers.cs
new file mode 100644
index 0000000..fda9c8d
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/PathHelpers.cs
@@ -0,0 +1,15 @@
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal static class PathHelpers
+{
+ public static string GetRootDirectory()
+ {
+ var directory = Environment.CurrentDirectory;
+ while (directory != null && !Directory.Exists(Path.Combine(directory, ".git")))
+ {
+ directory = Path.GetDirectoryName(directory);
+ }
+
+ return directory ?? throw new InvalidOperationException("Cannot find the root of the git repository");
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/ProjectBuilder.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/ProjectBuilder.cs
new file mode 100644
index 0000000..ac33beb
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/ProjectBuilder.cs
@@ -0,0 +1,100 @@
+using System.Xml.Linq;
+using Xunit.Abstractions;
+using System.Text.Json;
+using CliWrap;
+
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class ProjectBuilder : IDisposable
+{
+ private const string SarifFileName = "BuildOutput.sarif";
+
+ private readonly TemporaryDirectory _directory;
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public ProjectBuilder(PackageFixture fixture, ITestOutputHelper testOutputHelper)
+ {
+ this._testOutputHelper = testOutputHelper;
+
+ this._directory = TemporaryDirectory.Create();
+ this._directory.CreateTextFile("NuGet.config", $"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+
+ File.Copy(Path.Combine(PathHelpers.GetRootDirectory(), "global.json"), this._directory.GetPath("global.json"));
+ }
+
+ public void AddFile(string relativePath, string content)
+ {
+ File.WriteAllText(this._directory.GetPath(relativePath), content);
+ }
+
+ public void AddCsprojFile(Dictionary? properties = null)
+ {
+ var element = new XElement("PropertyGroup");
+ if (properties != null)
+ {
+ foreach (var prop in properties)
+ {
+ element.Add(new XElement(prop.Key), prop.Value);
+ }
+ }
+
+ var content = $"""
+
+
+ exe
+ net$(NETCoreAppMaximumVersion)
+ enable
+ enable
+ {SarifFileName},version=2.1
+
+ {element}
+
+
+
+
+
+ """;
+
+ File.WriteAllText(this._directory.GetPath("test.csproj"), content);
+ }
+
+ public async Task BuildAndGetOutput(string[]? buildArguments = null)
+ {
+ var result = await Cli.Wrap("dotnet")
+ .WithWorkingDirectory(this._directory.FullPath)
+ .WithArguments(["build", .. (buildArguments ?? [])])
+ .WithEnvironmentVariables(env => env.Set("CI", null).Set("GITHUB_ACTIONS", null))
+ .WithStandardOutputPipe(PipeTarget.ToDelegate(this._testOutputHelper.WriteLine))
+ .WithStandardErrorPipe(PipeTarget.ToDelegate(this._testOutputHelper.WriteLine))
+ .WithValidation(CommandResultValidation.None)
+ .ExecuteAsync();
+
+ this._testOutputHelper.WriteLine("Process exit code: " + result.ExitCode);
+
+ var bytes = await File.ReadAllBytesAsync(this._directory.GetPath(SarifFileName));
+ var sarif = JsonSerializer.Deserialize(bytes) ?? throw new InvalidOperationException("The sarif file is invalid");
+ this._testOutputHelper.WriteLine("Sarif result:\n" + string.Join("\n", sarif.AllResults().Select(r => r.ToString())));
+ return sarif;
+ }
+
+ public void Dispose() => this._directory.Dispose();
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFile.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFile.cs
new file mode 100644
index 0000000..f0aa47b
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFile.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class SarifFile
+{
+ [JsonPropertyName("runs")]
+ public SarifFileRun[]? Runs { get; set; }
+
+ public IEnumerable AllResults() => this.Runs?.SelectMany(r => r.Results ?? []) ?? [];
+
+ public bool HasError() => this.AllResults().Any(r => r.Level == "error");
+ public bool HasError(string ruleId) => this.AllResults().Any(r => r.Level == "error" && r.RuleId == ruleId);
+ public bool HasWarning(string ruleId) => this.AllResults().Any(r => r.Level == "warning" && r.RuleId == ruleId);
+ public bool HasNote(string ruleId) => this.AllResults().Any(r => r.Level == "note" && r.RuleId == ruleId);
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRun.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRun.cs
new file mode 100644
index 0000000..16b1bc4
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRun.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class SarifFileRun
+{
+ [JsonPropertyName("results")]
+ public SarifFileRunResult[]? Results { get; set; }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResult.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResult.cs
new file mode 100644
index 0000000..5b83c72
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResult.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class SarifFileRunResult
+{
+ [JsonPropertyName("ruleId")]
+ public string? RuleId { get; set; }
+
+ [JsonPropertyName("level")]
+ public string? Level { get; set; }
+
+ [JsonPropertyName("message")]
+ public SarifFileRunResultMessage? Message { get; set; }
+
+ public override string ToString()
+ {
+ return $"{this.Level}:{this.RuleId} {this.Message}";
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResultMessage.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResultMessage.cs
new file mode 100644
index 0000000..25fc30d
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SarifFileRunResultMessage.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class SarifFileRunResultMessage
+{
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
+ public override string ToString()
+ {
+ return this.Text ?? "";
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SharedHttpClient.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SharedHttpClient.cs
new file mode 100644
index 0000000..bb29c4a
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/SharedHttpClient.cs
@@ -0,0 +1,73 @@
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+internal static class SharedHttpClient
+{
+ public static HttpClient Instance { get; } = CreateHttpClient();
+
+ private static HttpClient CreateHttpClient()
+ {
+ var socketHandler = new SocketsHttpHandler()
+ {
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
+ PooledConnectionLifetime = TimeSpan.FromMinutes(1),
+ };
+
+ return new HttpClient(new HttpRetryMessageHandler(socketHandler), disposeHandler: true);
+ }
+ private sealed class HttpRetryMessageHandler(HttpMessageHandler handler) : DelegatingHandler(handler)
+ {
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ const int maxRetries = 5;
+ var defaultDelay = TimeSpan.FromMilliseconds(200);
+ for (var i = 1; ; i++, defaultDelay *= 2)
+ {
+ TimeSpan? delayHint = null;
+ HttpResponseMessage? result = null;
+
+ try
+ {
+ result = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ if (!IsLastAttempt(i) && ((int)result.StatusCode >= 500 || result.StatusCode is System.Net.HttpStatusCode.RequestTimeout or System.Net.HttpStatusCode.TooManyRequests))
+ {
+ // Use "Retry-After" value, if available. Typically, this is sent with
+ // either a 503 (Service Unavailable) or 429 (Too Many Requests):
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+
+ delayHint = result.Headers.RetryAfter switch
+ {
+ { Date: { } date } => date - DateTimeOffset.UtcNow,
+ { Delta: { } delta } => delta,
+ _ => null,
+ };
+
+ result.Dispose();
+ }
+ else
+ {
+ return result;
+ }
+ }
+ catch (HttpRequestException)
+ {
+ result?.Dispose();
+ if (IsLastAttempt(i))
+ {
+ throw;
+ }
+ }
+ catch (TaskCanceledException ex) when (ex.CancellationToken != cancellationToken) // catch "The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing"
+ {
+ result?.Dispose();
+ if (IsLastAttempt(i))
+ {
+ throw;
+ }
+ }
+
+ await Task.Delay(delayHint is { } someDelay && someDelay > TimeSpan.Zero ? someDelay : defaultDelay, cancellationToken).ConfigureAwait(false);
+
+ static bool IsLastAttempt(int i) => i >= maxRetries;
+ }
+ }
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/TemporaryDirectory.cs b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/TemporaryDirectory.cs
new file mode 100644
index 0000000..0803e7e
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Helpers/TemporaryDirectory.cs
@@ -0,0 +1,39 @@
+namespace Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+internal sealed class TemporaryDirectory : IDisposable
+{
+ private TemporaryDirectory(string fullPath) => this.FullPath = fullPath;
+
+ public string FullPath { get; }
+
+ public static TemporaryDirectory Create()
+ {
+ var path = Path.GetFullPath(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")));
+ _ = Directory.CreateDirectory(path);
+ return new TemporaryDirectory(path);
+ }
+
+ public string GetPath(string relativePath)
+ {
+ return Path.Combine(this.FullPath, relativePath);
+ }
+
+ public void CreateTextFile(string relativePath, string content)
+ {
+ var path = this.GetPath(relativePath);
+ _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ File.WriteAllText(path, content);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(this.FullPath, recursive: true);
+ }
+ catch
+ {
+ // We use this code in tests, so it's not important if a folder cannot be deleted
+ }
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/PackageFixture.cs b/tests/Workleap.DotNet.CodingStandards.Tests/PackageFixture.cs
new file mode 100644
index 0000000..2c8df3e
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/PackageFixture.cs
@@ -0,0 +1,48 @@
+using CliWrap;
+using CliWrap.Buffered;
+using Workleap.DotNet.CodingStandards.Tests.Helpers;
+
+namespace Workleap.DotNet.CodingStandards.Tests;
+
+public sealed class PackageFixture : IAsyncLifetime
+{
+ private readonly TemporaryDirectory _packageDirectory = TemporaryDirectory.Create();
+
+ public string PackageDirectory => this._packageDirectory.FullPath;
+
+ public async Task InitializeAsync()
+ {
+ var nuspecPath = Path.Combine(PathHelpers.GetRootDirectory(), "Workleap.DotNet.CodingStandards.nuspec");
+ string[] args = ["pack", nuspecPath, "-ForceEnglishOutput", "-Version", "999.9.9", "-OutputDirectory", this._packageDirectory.FullPath];
+
+ if (OperatingSystem.IsWindows())
+ {
+ var exe = Path.Combine(Path.GetTempPath(), $"nuget-{Guid.NewGuid()}.exe");
+ await DownloadFileAsync("https://dist.nuget.org/win-x86-commandline/latest/nuget.exe", exe);
+
+ _ = await Cli.Wrap(exe)
+ .WithArguments(args)
+ .ExecuteAsync();
+ }
+ else
+ {
+ _ = await Cli.Wrap("nuget")
+ .WithArguments(args)
+ .ExecuteBufferedAsync();
+ }
+ }
+
+ public Task DisposeAsync()
+ {
+ this._packageDirectory.Dispose();
+ return Task.CompletedTask;
+ }
+
+ private static async Task DownloadFileAsync(string url, string path)
+ {
+ _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ await using var nugetStream = await SharedHttpClient.Instance.GetStreamAsync(url);
+ await using var fileStream = File.Create(path);
+ await nugetStream.CopyToAsync(fileStream);
+ }
+}
diff --git a/tests/Workleap.DotNet.CodingStandards.Tests/Workleap.DotNet.CodingStandards.Tests.csproj b/tests/Workleap.DotNet.CodingStandards.Tests/Workleap.DotNet.CodingStandards.Tests.csproj
new file mode 100644
index 0000000..8c7d341
--- /dev/null
+++ b/tests/Workleap.DotNet.CodingStandards.Tests/Workleap.DotNet.CodingStandards.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ false
+ true
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/wl-dotnet-codingstandards.sln b/wl-dotnet-codingstandards.sln
new file mode 100644
index 0000000..56e8104
--- /dev/null
+++ b/wl-dotnet-codingstandards.sln
@@ -0,0 +1,30 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C1F5A879-3A26-4621-ADB7-3B1C59CBC8B3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Workleap.DotNet.CodingStandards.Tests", "tests\Workleap.DotNet.CodingStandards.Tests\Workleap.DotNet.CodingStandards.Tests.csproj", "{640037BA-49DF-4BBD-9858-3DC89E2739FD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {640037BA-49DF-4BBD-9858-3DC89E2739FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {640037BA-49DF-4BBD-9858-3DC89E2739FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {640037BA-49DF-4BBD-9858-3DC89E2739FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {640037BA-49DF-4BBD-9858-3DC89E2739FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {640037BA-49DF-4BBD-9858-3DC89E2739FD} = {C1F5A879-3A26-4621-ADB7-3B1C59CBC8B3}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {18E64023-58E9-4BA2-BCE7-4BD5E1A023C1}
+ EndGlobalSection
+EndGlobal