diff --git a/CHANGELOG.md b/CHANGELOG.md index ab50821910e..96d6f26c032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ ## 8.0.0-alpha.1 -- The first public preview of [Polly v8](https://github.com/App-vNext/Polly/issues/1048) with our [new high-performance core API](https://github.com/App-vNext/Polly/blob/main/src/Polly.Core/README.md) and extensions - Thanks to: +- The first public preview of [Polly v8](https://github.com/App-vNext/Polly/issues/1048) with our [new high-performance core API](https://github.com/App-vNext/Polly/blob/main/src/Polly.Core/README.md) and extensions. Feel free to check out the [samples](samples/) to see the new and improved Polly V8 in action. +- The first release of the new NuGet packages: + - [`Polly.Core`](https://nuget.org/packages/Polly.Core) - This package contains the new Polly V8 API. + - [`Polly.Extensions`](https://nuget.org/packages/Polly.Extensions) - This package is designed to integrate Polly with dependency injection and enable telemetry. + - [`Polly.RateLimiting`](https://nuget.org/packages/Polly.RateLimiting) - This package provides an integration between Polly and [`System.Threading.RateLimiting`](https://www.nuget.org/packages/System.Threading.RateLimiting/). + +Thanks to: - [@adamnova](https://github.com/adamnova) - [@andrey-noskov](https://github.com/andrey-noskov) - [@joelhulen](https://github.com/joelhulen) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56bc382e122..ee7e1d5f9ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,7 @@ true + 8.0.0-alpha.1 @@ -8,6 +9,7 @@ + @@ -17,6 +19,9 @@ + + + diff --git a/samples/.vscode/settings.json b/samples/.vscode/settings.json new file mode 100644 index 00000000000..f612142721b --- /dev/null +++ b/samples/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Samples.sln" +} \ No newline at end of file diff --git a/samples/DependencyInjection/DependencyInjection.csproj b/samples/DependencyInjection/DependencyInjection.csproj new file mode 100644 index 00000000000..f4628e7cdee --- /dev/null +++ b/samples/DependencyInjection/DependencyInjection.csproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + diff --git a/samples/DependencyInjection/Program.cs b/samples/DependencyInjection/Program.cs new file mode 100644 index 00000000000..c75982ddf15 --- /dev/null +++ b/samples/DependencyInjection/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Registry; +using Polly.Timeout; + +// ------------------------------------------------------------------------ +// 1. Register your resilience strategy +// ------------------------------------------------------------------------ + +var serviceProvider = new ServiceCollection() + .AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)) + // Use "AddResilienceStrategy" extension method to configure your named strategy + .AddResilienceStrategy("my-strategy", (builder, context) => + { + // You can resolve any service from DI when building the strategy + context.ServiceProvider.GetRequiredService(); + + builder.AddTimeout(TimeSpan.FromSeconds(1)); + }) + // You can also register result-based (generic) resilience strategies + // First generic parameter is the key type, the second one is the result type + // This overload does not use the context argument (simple scenarios) + .AddResilienceStrategy("my-strategy", builder => + { + builder.AddTimeout(TimeSpan.FromSeconds(1)); + }) + .BuildServiceProvider(); + +// ------------------------------------------------------------------------ +// 2. Retrieve and use your resilience strategy +// ------------------------------------------------------------------------ + +// Resolve the resilience strategy provider for string-based keys +ResilienceStrategyProvider strategyProvider = serviceProvider.GetRequiredService>(); + +// Retrieve the strategy by name +ResilienceStrategy strategy = strategyProvider.Get("my-strategy"); + +// Retrieve the generic strategy by name +ResilienceStrategy genericStrategy = strategyProvider.Get("my-strategy"); + +try +{ + // Execute the strategy + // Notice in console output that telemetry is automatically enabled + await strategy.ExecuteAsync(async token => await Task.Delay(10000, token), CancellationToken.None); +} +catch (TimeoutRejectedException) +{ + // The timeout strategy cancels the user callback and throws this exception + Console.WriteLine("Timeout!"); +} diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 00000000000..cb53b3c00ba --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,8 @@ + + + true + true + true + latest + + diff --git a/samples/Directory.Build.targets b/samples/Directory.Build.targets new file mode 100644 index 00000000000..85395b0df3c --- /dev/null +++ b/samples/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/samples/Extensibility/Extensibility.csproj b/samples/Extensibility/Extensibility.csproj new file mode 100644 index 00000000000..201d8ee38e0 --- /dev/null +++ b/samples/Extensibility/Extensibility.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/samples/Extensibility/Program.cs b/samples/Extensibility/Program.cs new file mode 100644 index 00000000000..e59e9f006ed --- /dev/null +++ b/samples/Extensibility/Program.cs @@ -0,0 +1,135 @@ +using Polly; +using Polly.Telemetry; + +// ------------------------------------------------------------------------ +// Usage of custom strategy +// ------------------------------------------------------------------------ +var strategy = new ResilienceStrategyBuilder() + // This is custom extension defined in this sample + .AddMyResilienceStrategy(new MyResilienceStrategyOptions + { + OnCustomEvent = args => + { + Console.WriteLine("OnCustomEvent"); + return default; + } + }) + .Build(); + +// Execute the strategy +strategy.Execute(() => { }); + +// ------------------------------------------------------------------------ +// SIMPLE EXTENSIBILITY MODEL (INLINE STRATEGY) +// ------------------------------------------------------------------------ + +strategy = new ResilienceStrategyBuilder() + // Just add the strategy instance directly + .AddStrategy(new MySimpleStrategy()) + .Build(); + +// Execute the strategy +strategy.Execute(() => { }); + +internal class MySimpleStrategy : ResilienceStrategy +{ + protected override ValueTask> ExecuteCoreAsync(Func>> callback, ResilienceContext context, TState state) + { + Console.WriteLine("MySimpleStrategy executing!"); + + // The "context" holds information about execution mode + Console.WriteLine("context.IsSynchronous: {0}", context.IsSynchronous); + Console.WriteLine("context.ResultType: {0}", context.ResultType); + Console.WriteLine("context.IsVoid: {0}", context.IsVoid); + + // The "state" is an ambient value passed by the caller that holds the state. + // Here, we do not do anything with it, just pass it to the callback. + + // Execute the provided callback + return callback(context, state); + } +} + +// ------------------------------------------------------------------------ +// STANDARD EXTENSIBILITY MODEL +// ------------------------------------------------------------------------ + +// ------------------------------------------------------------------------ +// 1. Create options for your custom strategy +// ------------------------------------------------------------------------ + +// 1.A Define arguments for events that your strategy uses (optional) +public readonly record struct OnCustomEventArguments(ResilienceContext Context); + +// 1.B Define the options. +public class MyResilienceStrategyOptions : ResilienceStrategyOptions +{ + public override string StrategyType => "MyCustomStrategy"; + + // Use the arguments in the delegates. + // The recommendation is to use asynchronous delegates. + public Func? OnCustomEvent { get; set; } +} + +// ------------------------------------------------------------------------ +// 2. Create a custom resilience strategy that derives from ResilienceStrategy +// ------------------------------------------------------------------------ + +// The strategy should be internal and not exposed as part of any public API. Instead, expose options and extensions for resilience strategy builder. +internal class MyResilienceStrategy : ResilienceStrategy +{ + private readonly ResilienceStrategyTelemetry telemetry; + private readonly Func? onCustomEvent; + + public MyResilienceStrategy(ResilienceStrategyTelemetry telemetry, MyResilienceStrategyOptions options) + { + this.telemetry = telemetry; + this.onCustomEvent = options.OnCustomEvent; + } + + protected override async ValueTask> ExecuteCoreAsync(Func>> callback, ResilienceContext context, TState state) + { + // Here, do something before callback execution + // ... + + // Execute the provided callback + var outcome = await callback(context, state); + + // Here, do something after callback execution + // ... + + // You can then report important telemetry events + telemetry.Report("MyCustomEvent", context, new OnCustomEventArguments(context)); + + // Call the delegate if provided by the user + if (onCustomEvent is not null) + { + await onCustomEvent(new OnCustomEventArguments(context)); + } + + return outcome; + } +} + +// ------------------------------------------------------------------------ +// 3. Expose new extensions for ResilienceStrategyBuilder +// ------------------------------------------------------------------------ + +public static class MyResilienceStrategyExtensions +{ + // Add new extension that works for both "ResilienceStrategyBuilder" and "ResilienceStrategyBuilder" + public static TBuilder AddMyResilienceStrategy(this TBuilder builder, MyResilienceStrategyOptions options) + where TBuilder : ResilienceStrategyBuilderBase + { + builder.AddStrategy( + // Provide a factory that creates the strategy + context => new MyResilienceStrategy(context.Telemetry, options), + + // Pass the options, note that the options instance is automatically validated by the builder + options); + + return builder; + } +} + + diff --git a/samples/GenericStrategies/GenericStrategies.csproj b/samples/GenericStrategies/GenericStrategies.csproj new file mode 100644 index 00000000000..ee418caeadd --- /dev/null +++ b/samples/GenericStrategies/GenericStrategies.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/samples/GenericStrategies/Program.cs b/samples/GenericStrategies/Program.cs new file mode 100644 index 00000000000..7aa5bb22319 --- /dev/null +++ b/samples/GenericStrategies/Program.cs @@ -0,0 +1,75 @@ +using Polly; +using Polly.Fallback; +using Polly.Retry; +using Polly.Timeout; +using System.Net; + +// ---------------------------------------------------------------------------- +// Create a generic resilience strategy using ResilienceStrategyBuilder +// ---------------------------------------------------------------------------- + +// The generic ResilienceStrategyBuilder creates a ResilienceStrategy +// that can execute synchronous and asynchronous callbacks that return T. + +ResilienceStrategy strategy = new ResilienceStrategyBuilder() + .AddFallback(new FallbackStrategyOptions + { + FallbackAction = async _ => + { + await Task.Delay(10); + + // Return fallback result + return new Outcome(new HttpResponseMessage(System.Net.HttpStatusCode.OK)); + }, + // You can also use switch expressions for succinct syntax + ShouldHandle = outcome => outcome switch + { + // The "PredicateResult.True" is shorthand to "new ValueTask(true)" + { Exception: HttpRequestException } => PredicateResult.True, + { Result: HttpResponseMessage response } when response.StatusCode == HttpStatusCode.InternalServerError => PredicateResult.True, + _ => PredicateResult.False + }, + OnFallback = _ => { Console.WriteLine("Fallback!"); return default; } + }) + .AddRetry(new RetryStrategyOptions + { + ShouldRetry = outcome => + { + // We can handle specific result + if (outcome.Result?.StatusCode == HttpStatusCode.InternalServerError) + { + return PredicateResult.True; + } + + // Or exception + if ( outcome.Exception is HttpRequestException) + { + return PredicateResult.True; + } + + return PredicateResult.False; + }, + // Register user callback called whenever retry occurs + OnRetry = outcome => { Console.WriteLine($"Retrying '{outcome.Result?.StatusCode}'..."); return default; }, + BaseDelay = TimeSpan.FromMilliseconds(400), + BackoffType = RetryBackoffType.Constant, + RetryCount = 3 + }) + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromMilliseconds(500), + // Register user callback called whenever timeout occurs + OnTimeout = _ => { Console.WriteLine("Timeout occurred!"); return default; } + }) + .Build(); + +var response = await strategy.ExecuteAsync( + async token => + { + await Task.Delay(10); + // This causes the action fail, thus using the fallback strategy above + return new HttpResponseMessage(HttpStatusCode.InternalServerError); + }, + CancellationToken.None); + +Console.WriteLine($"Response: {response.StatusCode}"); diff --git a/samples/Intro/Intro.csproj b/samples/Intro/Intro.csproj new file mode 100644 index 00000000000..8321197231a --- /dev/null +++ b/samples/Intro/Intro.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/samples/Intro/Program.cs b/samples/Intro/Program.cs new file mode 100644 index 00000000000..91fe0100080 --- /dev/null +++ b/samples/Intro/Program.cs @@ -0,0 +1,82 @@ +using Polly; +using Polly.Retry; +using Polly.Timeout; + +// ------------------------------------------------------------------------ +// 1. Create a simple resilience strategy using ResilienceStrategyBuilder +// ------------------------------------------------------------------------ + +// The ResilienceStrategyBuilder creates a ResilienceStrategy +// that can be executed synchronously or asynchronously +// and for both void and result-returning user-callbacks. +ResilienceStrategy strategy = new ResilienceStrategyBuilder() + // Use convenience extension that accepts TimeSpan + .AddTimeout(TimeSpan.FromSeconds(5)) + .Build(); + +// ------------------------------------------------------------------------ +// 2. Execute the strategy +// ------------------------------------------------------------------------ + +// Synchronously +strategy.Execute(() => { }); + +// Asynchronously +await strategy.ExecuteAsync(async token => { await Task.Delay(10); }, CancellationToken.None); + +// Synchronously with result +strategy.Execute(token => "some-result"); + +// Asynchronously with result +await strategy.ExecuteAsync(async token => { await Task.Delay(10); return "some-result"; }, CancellationToken.None); + +// Use state to avoid lambda allocation +strategy.Execute(static state => state, "my-state"); + +// ------------------------------------------------------------------------ +// 3. Create and execute a pipeline of strategies +// ------------------------------------------------------------------------ + +strategy = new ResilienceStrategyBuilder() + // Add retries using the options + .AddRetry(new RetryStrategyOptions + { + ShouldRetry = outcome => + { + // We want to retry on this specific exception + if (outcome.Exception is TimeoutRejectedException) + { + // The "PredicateResult.True" is shorthand to "new ValueTask(true)" + return PredicateResult.True; + } + + return PredicateResult.False; + }, + // Register user callback called whenever retry occurs + OnRetry = args => { Console.WriteLine($"Retrying...{args.Arguments.Attempt} attempt"); return default; }, + BaseDelay = TimeSpan.FromMilliseconds(400), + BackoffType = RetryBackoffType.Constant, + RetryCount = 3 + }) + // Add timeout using the options + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromMilliseconds(500), + // Register user callback called whenever timeout occurs + OnTimeout = args => + { + + Console.WriteLine($"Timeout occurred after {args.Timeout}!"); + return default; + } + }) + .Build(); + +try +{ + await strategy.ExecuteAsync(async token => await Task.Delay(TimeSpan.FromSeconds(2), token), CancellationToken.None); +} +catch (TimeoutRejectedException) +{ + // ok, expected +} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000000..a327353d3df --- /dev/null +++ b/samples/README.md @@ -0,0 +1,11 @@ +# Polly Samples + +This repository contains a solution with basic examples demonstrating the creation and utilization of Polly strategies. + +- [`Intro`](/Intro) - This section serves as an introduction to Polly. It demonstrates how to use `ResilienceStrategyBuilder` to create a `ResilienceStrategy`, which can be used to execute various user-provided callbacks. +- [`GenericStrategies`](/GenericStrategies) - This example showcases how to use `ResilienceStrategyBuilder` to create a generic `ResilienceStrategy`. +- [`Retries`](/Retries) - This part explains how to configure a retry resilience strategy. +- [`Extensibility`](/Extensibility) - In this part, you can learn how Polly can be extended with custom resilience strategies. +- [`DependencyInjection`](/DependencyInjection) - This section demonstrates the integration of Polly with `IServiceCollection`. + +These examples are designed as a quick-start guide to Polly. If you wish to explore more advanced scenarios and further enhance your learning, consider visiting the [Polly-Samples](https://github.com/App-vNext/Polly-Samples) repository. diff --git a/samples/Retries/Program.cs b/samples/Retries/Program.cs new file mode 100644 index 00000000000..c567461ae5e --- /dev/null +++ b/samples/Retries/Program.cs @@ -0,0 +1,114 @@ +using Polly; +using Polly.Retry; +using System.Net; + +// ------------------------------------------------------------------------ +// 1. Create a retry strategy that only handles invalid operation exceptions +// ------------------------------------------------------------------------ + +ResilienceStrategy strategy = new ResilienceStrategyBuilder() + .AddRetry(new RetryStrategyOptions + { + // Specify what exceptions should be retried + ShouldRetry = outcome => + { + if (outcome.Exception is InvalidOperationException) + { + return PredicateResult.True; + } + + return PredicateResult.False; + }, + }) + .Build(); + +// ------------------------------------------------------------------------ +// 2. Customize the retry behavior +// ------------------------------------------------------------------------ + +strategy = new ResilienceStrategyBuilder() + .AddRetry(new RetryStrategyOptions + { + // Specify what exceptions should be retried + ShouldRetry = outcome => + { + if (outcome.Exception is InvalidOperationException) + { + return PredicateResult.True; + } + + return PredicateResult.False; + }, + RetryCount = 4, + BaseDelay = TimeSpan.FromSeconds(1), + + // The recommended backoff type for HTTP scenarios + // See here for more information: https://github.com/App-vNext/Polly/wiki/Retry-with-jitter#more-complex-jitter + BackoffType = RetryBackoffType.ExponentialWithJitter + }) + .Build(); + +// ------------------------------------------------------------------------ +// 3. Register the callbacks +// ------------------------------------------------------------------------ + +strategy = new ResilienceStrategyBuilder() + .AddRetry(new RetryStrategyOptions + { + // Specify what exceptions should be retried + ShouldRetry = outcome => + { + if (outcome.Exception is InvalidOperationException) + { + return PredicateResult.True; + } + + return PredicateResult.False; + }, + + OnRetry = outcome => + { + Console.WriteLine($"Retrying attempt {outcome.Arguments.Attempt}..."); + return default; + } + }) + .Build(); + +// ------------------------------------------------------------------------ +// 4. Create an HTTP retry strategy that handles both exceptions and results +// ------------------------------------------------------------------------ + +ResilienceStrategy httpStrategy = new ResilienceStrategyBuilder() + .AddRetry(new RetryStrategyOptions + { + // Specify what exceptions or results should be retried + ShouldRetry = outcome => + { + // Now, also handle results + if (outcome.Result?.StatusCode == HttpStatusCode.InternalServerError) + { + return PredicateResult.True; + } + + if (outcome.Exception is InvalidOperationException) + { + return PredicateResult.True; + } + + return PredicateResult.False; + }, + + // Specify delay generator + RetryDelayGenerator = outcome => + { + if (outcome.Result is not null && outcome.Result.Headers.TryGetValues("Retry-After", out var value)) + { + // Return delay based on header + return new ValueTask(TimeSpan.FromSeconds(int.Parse(value.Single()))); + } + + // Return delay hinted by the retry strategy + return new ValueTask(outcome.Arguments.DelayHint); + } + }) + .Build(); diff --git a/samples/Retries/Retries.csproj b/samples/Retries/Retries.csproj new file mode 100644 index 00000000000..201d8ee38e0 --- /dev/null +++ b/samples/Retries/Retries.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/samples/Samples.sln b/samples/Samples.sln new file mode 100644 index 00000000000..056777613b5 --- /dev/null +++ b/samples/Samples.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CE7916FD-6C1A-48CE-8919-F4BAB4E3770F}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extensibility", "Extensibility\Extensibility.csproj", "{1EC623B0-2B11-427B-A2B6-A22265E5C941}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Intro", "Intro\Intro.csproj", "{D23FC7B1-B549-405A-823C-CF43382C3432}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericStrategies", "GenericStrategies\GenericStrategies.csproj", "{10175C17-01A5-4936-8966-86FB1C7891C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Retries", "Retries\Retries.csproj", "{8A46294C-29CB-4E70-BFE0-5DE386437C50}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection", "DependencyInjection\DependencyInjection.csproj", "{9B8BFE03-4457-4C55-91AD-4096DDE622C3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1EC623B0-2B11-427B-A2B6-A22265E5C941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EC623B0-2B11-427B-A2B6-A22265E5C941}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EC623B0-2B11-427B-A2B6-A22265E5C941}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EC623B0-2B11-427B-A2B6-A22265E5C941}.Release|Any CPU.Build.0 = Release|Any CPU + {D23FC7B1-B549-405A-823C-CF43382C3432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D23FC7B1-B549-405A-823C-CF43382C3432}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D23FC7B1-B549-405A-823C-CF43382C3432}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D23FC7B1-B549-405A-823C-CF43382C3432}.Release|Any CPU.Build.0 = Release|Any CPU + {10175C17-01A5-4936-8966-86FB1C7891C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10175C17-01A5-4936-8966-86FB1C7891C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10175C17-01A5-4936-8966-86FB1C7891C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10175C17-01A5-4936-8966-86FB1C7891C0}.Release|Any CPU.Build.0 = Release|Any CPU + {8A46294C-29CB-4E70-BFE0-5DE386437C50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A46294C-29CB-4E70-BFE0-5DE386437C50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A46294C-29CB-4E70-BFE0-5DE386437C50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A46294C-29CB-4E70-BFE0-5DE386437C50}.Release|Any CPU.Build.0 = Release|Any CPU + {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B8BFE03-4457-4C55-91AD-4096DDE622C3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67CA3053-C929-4CAE-99A3-40CF1894FE2B} + EndGlobalSection +EndGlobal