diff --git a/Polly8Study.Core/GlobalUsing.cs b/Polly8Study.Core/GlobalUsing.cs new file mode 100644 index 0000000..eb4d3ae --- /dev/null +++ b/Polly8Study.Core/GlobalUsing.cs @@ -0,0 +1,24 @@ +global using System.IO; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Collections.Immutable; +global using System.Collections.Specialized; +global using System.Configuration; +global using System.Diagnostics; +global using System.Dynamic; +global using System.Threading; +global using System.Threading.Tasks; + +global using Polly; +global using Polly.CircuitBreaker; +global using Polly.Fallback; +global using Polly.Retry; +global using Polly.Timeout; +global using Polly.Wrap; +global using Polly.Extensions.Http; +global using Polly.Bulkhead; + +global using Polly8Study.Core; + + diff --git a/Polly8Study.Core/HttpClientUtil.cs b/Polly8Study.Core/HttpClientUtil.cs new file mode 100644 index 0000000..20be02b --- /dev/null +++ b/Polly8Study.Core/HttpClientUtil.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Polly8Study.Core +{ + public class HttpClientUtil + { + } +} diff --git a/Polly8Study.Core/Polly8Study.Core.csproj b/Polly8Study.Core/Polly8Study.Core.csproj new file mode 100644 index 0000000..8454850 --- /dev/null +++ b/Polly8Study.Core/Polly8Study.Core.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/Polly8Study.Test/GlobalUsing.cs b/Polly8Study.Test/GlobalUsing.cs new file mode 100644 index 0000000..cd4383f --- /dev/null +++ b/Polly8Study.Test/GlobalUsing.cs @@ -0,0 +1,29 @@ +global using System.IO; +global using System.Collections; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Collections.Immutable; +global using System.Collections.Specialized; +global using System.Configuration; +global using System.Diagnostics; +global using System.Dynamic; +global using System.Threading; +global using System.Threading.Tasks; + +global using Xunit; +global using Xunit.Sdk; +global using Xunit.Extensions; +global using Xunit.Abstractions; + +global using Polly; +global using Polly.CircuitBreaker; +global using Polly.Fallback; +global using Polly.Retry; +global using Polly.Timeout; +global using Polly.Wrap; +global using Polly.Extensions.Http; +global using Polly.Bulkhead; + +global using Polly8Study.Core; + + diff --git a/Polly8Study.Test/Polly8Study.Test.csproj b/Polly8Study.Test/Polly8Study.Test.csproj new file mode 100644 index 0000000..f3542b3 --- /dev/null +++ b/Polly8Study.Test/Polly8Study.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Polly8Study.Test/Polly8TimeoutTest.cs b/Polly8Study.Test/Polly8TimeoutTest.cs new file mode 100644 index 0000000..90bd86d --- /dev/null +++ b/Polly8Study.Test/Polly8TimeoutTest.cs @@ -0,0 +1,495 @@ +namespace Polly8Study.Test +{ + /// + /// Polly8ʱ + /// ؼ + /// CancellationTokenûDzʹõ + /// ֮ǰ汾еֹ۳ʱ۳ʱòȡ + /// + public class Polly8TimeoutTest + { + private readonly ITestOutputHelper _output; + + public Polly8TimeoutTest(ITestOutputHelper testOutput) + { + _output = testOutput; + } + + /// + /// ʱѡ + /// + [Fact] + public void TimeoutStrategyOptions_Test() + { + //ֱ + new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromMilliseconds(100)) + .Build(); + + //Ĭֵ + var defaultOption = new TimeoutStrategyOptions(); + + //ȫ + TimeoutStrategyOptions timeoutOption = new TimeoutStrategyOptions + { + //ΨһʶضԵضʵҲɸԲɵң + Name = "timeout", + + //̶ʱʱ(TimeoutGeneratorЧ) + Timeout = TimeSpan.FromSeconds(2), + + //̬ʱʱ + //TimeoutGenerator = arg => + //{ + // var ts = TimeSpan.FromSeconds(Math.Pow(2, 1)); + + // return ValueTask.FromResult(ts); + //}, + + //ʱʱijʱί + OnTimeout = (args) => + { + var key = args.Context.OperationKey; + _output.WriteLine("OnTimeout"); + return ValueTask.CompletedTask; + }, + }; + + //ʹ + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(timeoutOption) + .Build(); + } + + /// + /// ǿȡ񣺺Գʱ + /// ִͬ + /// + [Fact] + public void No_CancellationToken_Test() + { + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + //ⲿtcsʡ + var tcs = new CancellationTokenSource(); + try + { + pipeline.Execute + ( + //˴ǿȡ񣬷ʱΪ(һֱȴ) + callback: (CancellationToken innerToken) => + { + //ȻӿںʱΪûʹáȡġ Send󣬳ʱñԣȴӿȷ + HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost:44344/api/Timeout/Slow"); + + //ؼSend ûд cancellationToken + //ֻͬҪǿȡ + HttpResponseMessage response = new HttpClient().Send(requestMessage); + if (response.IsSuccessStatusCode == false) + { + _output.WriteLine($"Ӧ,Ӧ룺{response.StatusCode}"); + return; + } + var stream = response.Content.ReadAsStream(); + var bs = new byte[stream.Length]; + stream.Read(bs, 0, (int)stream.Length); + + var text = System.Text.UTF8Encoding.UTF8.GetString(bs); + + _output.WriteLine($"Ӧ:{text}"); + + _output.WriteLine("ִ"); + }, + cancellationToken: tcs.Token + ); + } + catch (OperationCanceledException ex) + { + _output.WriteLine($"ȡ쳣:{ex.Message}"); + } + catch (TimeoutRejectedException ex) + { + _output.WriteLine($"ʱ쳣:{ex.Message}"); + } + catch (Exception ex) + { + _output.WriteLine($"API쳣{ex.Message}"); + } + finally + { + tcs.TryReset(); + } + } + + /// + /// ȡ:ʱ + /// ִͬ + /// + [Fact] + public void Has_CancellationToken_Timeout_Test() + { + TimeoutStrategyOptions timeoutOption = new TimeoutStrategyOptions + { + //ΨһʶضԵضʵҲɸԲɵң + Name = "timeout", + + //̶ʱʱ(TimeoutGeneratorЧ) + Timeout = TimeSpan.FromSeconds(1), + + //̬ʱʱ + //TimeoutGenerator = arg => + //{ + // var ts = TimeSpan.FromSeconds(Math.Pow(2, 1)); + + // return ValueTask.FromResult(ts); + //}, + + //ʱʱijʱί + OnTimeout = (args) => + { + _output.WriteLine("OnTimeout ʱίִ"); + return ValueTask.CompletedTask; + }, + }; + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(timeoutOption) + .Build(); + + var tcs = new CancellationTokenSource(); + try + { + pipeline.Execute + ( + //˴ǿȡ񣬷ʱΪ(һֱȴ) + callback: (innerToken) => + { + HttpRequestMessage r = new HttpRequestMessage(HttpMethod.Get, "http://localhost:44344/api/Timeout/Slow"); + var response = new HttpClient().Send(r, innerToken); + if (response.IsSuccessStatusCode == false) + { + Console.WriteLine("Ӧ"); + return; + } + var stream = response.Content.ReadAsStream(); + var bs = new byte[stream.Length]; + stream.Read(bs, 0, (int)stream.Length); + + var text = System.Text.UTF8Encoding.UTF8.GetString(bs); + + _output.WriteLine($"Ӧ:{text}"); + _output.WriteLine("ִ"); + }, + cancellationToken: tcs.Token + ); + } + catch (OperationCanceledException ex) + { + _output.WriteLine($"ȡ쳣:{ex.Message}"); + } + catch (TimeoutRejectedException ex) + { + _output.WriteLine($"ʱ쳣:{ex.Message}"); + } + catch (Exception ex) + { + _output.WriteLine($"API쳣{ex.Message}"); + } + finally + { + tcs.TryReset(); + } + } + + /// + /// ȡ:ʱ + /// ִͬ + /// + [Fact] + public void Has_CancellationToken_NotTimeout_Test() + { + TimeoutStrategyOptions timeoutOption = new TimeoutStrategyOptions + { + //ΨһʶضԵضʵҲɸԲɵң + Name = "timeout", + + //̶ʱʱ(TimeoutGeneratorЧ) + Timeout = TimeSpan.FromSeconds(5), + + //̬ʱʱ + //TimeoutGenerator = arg => + //{ + // var ts = TimeSpan.FromSeconds(Math.Pow(2, 1)); + + // return ValueTask.FromResult(ts); + //}, + + //ʱʱijʱί + OnTimeout = (args) => + { + Console.WriteLine("OnTimeout ʱίִ"); + return ValueTask.CompletedTask; + }, + }; + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(timeoutOption) + .Build(); + + var tcs = new CancellationTokenSource(); + try + { + pipeline.Execute + ( + //˴ǿȡ񣬷ʱΪ(һֱȴ) + callback: (innerToken) => + { + HttpRequestMessage r = new HttpRequestMessage(HttpMethod.Get, "http://localhost:44344/api/Timeout/Slow"); + var response = new HttpClient().Send(r, innerToken); + if (response.IsSuccessStatusCode == false) + { + Console.WriteLine("Ӧ"); + return; + } + var stream = response.Content.ReadAsStream(); + var bs = new byte[stream.Length]; + stream.Read(bs, 0, (int)stream.Length); + + var text = System.Text.UTF8Encoding.UTF8.GetString(bs); + + _output.WriteLine($"Ӧ:{text}"); + _output.WriteLine("ִ"); + }, + cancellationToken: tcs.Token + ); + } + catch (OperationCanceledException ex) + { + _output.WriteLine($"ȡ쳣:{ex.Message}"); + } + catch (TimeoutRejectedException ex) + { + _output.WriteLine($"ʱ쳣:{ex.Message}"); + } + catch (Exception ex) + { + _output.WriteLine($"API쳣{ex.Message}"); + } + finally + { + tcs.TryReset(); + } + + } + + /// + /// ȡ:Գʱ + /// 첽ִ + /// + [Fact] + public async void No_CancellationToken_ExecuteAsync_Test() + { + TimeoutStrategyOptions timeoutOption = new TimeoutStrategyOptions + { + //ΨһʶضԵضʵҲɸԲɵң + Name = "timeout", + + //̶ʱʱ(TimeoutGeneratorЧ) + Timeout = TimeSpan.FromSeconds(1), + + + //ʱʱijʱί + OnTimeout = (args) => + { + _output.WriteLine("OnTimeout ʱίִ"); + return ValueTask.CompletedTask; + }, + }; + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(timeoutOption) + .Build(); + + var tcs = new CancellationTokenSource(); + try + { + await pipeline.ExecuteAsync + ( + //˴Ϊȡ񣬺Գʱ(һֱȴ) + callback: async (innerToken) => + { + //ɴԼ2࣬dzʱ1 + await Task.Delay (1000*3); + _output.WriteLine("ִ"); + }, + cancellationToken: tcs.Token + ); + } + catch (OperationCanceledException ex) + { + _output.WriteLine($"ȡ쳣:{ex.Message}"); + } + catch (TimeoutRejectedException ex) + { + _output.WriteLine($"ʱ쳣:{ex.Message}"); + } + catch (Exception ex) + { + _output.WriteLine($"API쳣{ex.Message}"); + } + finally + { + tcs.TryReset(); + } + } + + /// + /// ȡ:ʱ + /// 첽ִ + /// + /// + [Fact] + public async Task Has_CancellationToken_ExecuteAsync_Timeout_Test() + { + var pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + await Assert.ThrowsAnyAsync(async () => + { + var cts = new CancellationTokenSource(); + await pipeline.ExecuteAsync + ( + callback: async (innerToken) => + { + //ɴԼdzʱ1࣬3 + await Task.Delay(1000 * 3, innerToken); + _output.WriteLine("ִ"); + }, + cancellationToken: cts.Token + ); + }); + } + + /// + /// ȡ:ʱ + /// 첽ִ + /// + /// + [Fact] + public async Task Has_CancellationToken_ExecuteAsync_NoTimeout_Test() + { + var pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)) + .Build(); + + try + { + var cts = new CancellationTokenSource(); + await pipeline.ExecuteAsync + ( + callback: async (innerToken) => + { + //ʱɴԼʱ1 + await Task.Delay(1000, innerToken); + _output.WriteLine("ִ"); + }, + cancellationToken: cts.Token + ); + } + catch (Exception ex) + { + _output.WriteLine(ex.Message); + } + } + + // + /// ȡ:ʱ + /// 첽ִУToken + /// + /// + [Fact] + public async Task Has_OuterCancellationToken_ExecuteAsync_Timeout_Test() + { + var pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + + await Assert.ThrowsAnyAsync(async () => + { + await pipeline.ExecuteAsync + ( + callback: async (innerToken) => + { + //ɴԼdzʱ1࣬3 + await Task.Delay(1000 * 3, innerToken); + _output.WriteLine("ִ"); + } + ); + }); + } + + + [Fact] + public void SyncWithCancellationToken_Test() + { + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)) + .Build(); + + var tcs = new CancellationTokenSource(); + try + { + pipeline.Execute + ( + //˴ǿȡ񣬷ʱΪ(һֱȴ) + callback: SyncWithCancellationToken, + cancellationToken: tcs.Token + ); + } + catch (OperationCanceledException ex) + { + _output.WriteLine($"ȡ쳣:{ex.Message}"); + } + catch (TimeoutRejectedException ex) + { + _output.WriteLine($"ʱ쳣:{ex.Message}"); + } + catch (Exception ex) + { + _output.WriteLine($"API쳣{ex.Message}"); + } + finally + { + tcs.TryReset(); + } + } + + /// + /// ʹCancellationTokenͬ + /// + private void SyncWithCancellationToken(CancellationToken cancellationToken) + { + Thread thread = new Thread((token) => + { + int max = 500; + while (max > 0) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + max -= 1; + Thread.Sleep(10); + } + }); + + thread.Start(); + thread.Join(); + } + } +} \ No newline at end of file diff --git a/Polly8Study.Test/UseXunit.cs b/Polly8Study.Test/UseXunit.cs new file mode 100644 index 0000000..06b60ce --- /dev/null +++ b/Polly8Study.Test/UseXunit.cs @@ -0,0 +1,21 @@ +namespace Polly8Study.Test +{ + public class UseXunit + { + private readonly ITestOutputHelper _output; + + public UseXunit(ITestOutputHelper testOutput) + { + _output = testOutput; + } + + [Fact] + public void Use_xUnit_Test() + { + var msg = "ʹ xUnit ԪԿܣ"; + Assert.True(true, msg); + + _output.WriteLine(msg); + } + } +} \ No newline at end of file diff --git a/Polly8Study.WebApi/Controllers/TimeoutController.cs b/Polly8Study.WebApi/Controllers/TimeoutController.cs new file mode 100644 index 0000000..27c2629 --- /dev/null +++ b/Polly8Study.WebApi/Controllers/TimeoutController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Polly8Study.WebApi.Controllers +{ + [Route("api/[controller]/[action]")] + [ApiController] + public class TimeoutController : ControllerBase + { + private readonly ILogger _logger; + public TimeoutController(ILogger logger) + { + _logger = logger; + } + + /// + /// 快速接口 + /// + /// + [HttpGet] + public IActionResult Fast() + { + return Ok("Fast"); + } + + /// + /// 慢接口:耗时约2秒 + /// + /// + [HttpGet] + public async Task Slow(CancellationToken token) + { + //使用“异常过滤器”, 全局统一处理 + try + { + await Task.Delay(1000*2, token); + + return Ok("Slow"); + } + catch (OperationCanceledException ex) + { + Console.WriteLine("用户取消操作:" + ex.Message); + + throw; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + + return Problem("服务器异常"); + } + finally + { + Console.WriteLine("请求结束"); + } + } + } +} diff --git a/Polly8Study.WebApi/Filters/OperationCancelledExceptionFilter.cs b/Polly8Study.WebApi/Filters/OperationCancelledExceptionFilter.cs new file mode 100644 index 0000000..c423532 --- /dev/null +++ b/Polly8Study.WebApi/Filters/OperationCancelledExceptionFilter.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Polly8Study.WebApi.Filters +{ + /// + /// 用户取消调用(刷新浏览器等)异常过滤器 + /// + public class OperationCancelledExceptionFilter : IAsyncExceptionFilter + { + public Task OnExceptionAsync(ExceptionContext context) + { + if (context.Exception is OperationCanceledException ex) + { + var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + //context.ActionDescriptor.ContrallerName; + Console.WriteLine($"全局异常过滤器:{controllerActionDescriptor?.ControllerName}{controllerActionDescriptor?.ActionName} 请求,被用户取消!"); + } + + return Task.CompletedTask; + } + } +} diff --git a/Polly8Study.WebApi/Polly8Study.WebApi.csproj b/Polly8Study.WebApi/Polly8Study.WebApi.csproj new file mode 100644 index 0000000..9daa180 --- /dev/null +++ b/Polly8Study.WebApi/Polly8Study.WebApi.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Polly8Study.WebApi/Polly8Study.WebApi.http b/Polly8Study.WebApi/Polly8Study.WebApi.http new file mode 100644 index 0000000..7d6b3a4 --- /dev/null +++ b/Polly8Study.WebApi/Polly8Study.WebApi.http @@ -0,0 +1,6 @@ +@Polly8Study.WebApi_HostAddress = http://localhost:5000 + +GET {{Polly8Study.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Polly8Study.WebApi/Program.cs b/Polly8Study.WebApi/Program.cs new file mode 100644 index 0000000..8f38c3e --- /dev/null +++ b/Polly8Study.WebApi/Program.cs @@ -0,0 +1,50 @@ + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +using Polly8Study.WebApi.Filters; + +namespace Polly8Study.WebApi +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.Services.AddControllers(option => + { + option.Filters.Add(); + }); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + //builder.Services.AddSingleton(new OperationCancelledExceptionFilter); + + builder.Services.Configure(mvcOptions => + { + mvcOptions.Filters.Add(); + }); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAuthorization(); + + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/Polly8Study.WebApi/Properties/launchSettings.json b/Polly8Study.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..992dcfd --- /dev/null +++ b/Polly8Study.WebApi/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46344", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:44344", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Polly8Study.WebApi/appsettings.Development.json b/Polly8Study.WebApi/appsettings.Development.json new file mode 100644 index 0000000..3e1a225 --- /dev/null +++ b/Polly8Study.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + } +} diff --git a/Polly8Study.WebApi/appsettings.json b/Polly8Study.WebApi/appsettings.json new file mode 100644 index 0000000..4282acb --- /dev/null +++ b/Polly8Study.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Polly8Study.sln b/Polly8Study.sln new file mode 100644 index 0000000..5d7596a --- /dev/null +++ b/Polly8Study.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly8Study.Test", "Polly8Study.Test\Polly8Study.Test.csproj", "{6BA907CA-61B6-4956-A1FE-9F7596BC10B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Polly8Study.Core", "Polly8Study.Core\Polly8Study.Core.csproj", "{F8A1BC0D-978F-42A8-8747-5C8F635060C3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polly8Study.WebApi", "Polly8Study.WebApi\Polly8Study.WebApi.csproj", "{AEE3A7E0-DAD3-46ED-BF34-9D31A60CEFFD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6BA907CA-61B6-4956-A1FE-9F7596BC10B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BA907CA-61B6-4956-A1FE-9F7596BC10B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BA907CA-61B6-4956-A1FE-9F7596BC10B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BA907CA-61B6-4956-A1FE-9F7596BC10B9}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A1BC0D-978F-42A8-8747-5C8F635060C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A1BC0D-978F-42A8-8747-5C8F635060C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A1BC0D-978F-42A8-8747-5C8F635060C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A1BC0D-978F-42A8-8747-5C8F635060C3}.Release|Any CPU.Build.0 = Release|Any CPU + {AEE3A7E0-DAD3-46ED-BF34-9D31A60CEFFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEE3A7E0-DAD3-46ED-BF34-9D31A60CEFFD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEE3A7E0-DAD3-46ED-BF34-9D31A60CEFFD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEE3A7E0-DAD3-46ED-BF34-9D31A60CEFFD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0B24E74B-1EE9-4EEE-B199-8CB3018FA4D5} + EndGlobalSection +EndGlobal