diff --git a/src/Microser/Microser.API.Weather/Controllers/WeatherForecastController.cs b/src/Microser/Microser.API.Weather/Controllers/WeatherForecastController.cs index a6930dd..ca34570 100644 --- a/src/Microser/Microser.API.Weather/Controllers/WeatherForecastController.cs +++ b/src/Microser/Microser.API.Weather/Controllers/WeatherForecastController.cs @@ -6,7 +6,7 @@ namespace Microser.API.Weather.Controllers; [Authorize(Policy = "ClientIdPolicy")] [ApiController] -[Route("[controller]")] +[Route("api/[controller]")] public class WeatherForecastController : ControllerBase { private readonly ILogger _logger; diff --git a/src/Microser/Microser.API.Weather/Microser.API.Weather.csproj b/src/Microser/Microser.API.Weather/Microser.API.Weather.csproj index 75ce02e..e6f703b 100644 --- a/src/Microser/Microser.API.Weather/Microser.API.Weather.csproj +++ b/src/Microser/Microser.API.Weather/Microser.API.Weather.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/src/Microser/Microser.API.Weather/Program.cs b/src/Microser/Microser.API.Weather/Program.cs index f144cfa..a8ce7d2 100644 --- a/src/Microser/Microser.API.Weather/Program.cs +++ b/src/Microser/Microser.API.Weather/Program.cs @@ -1,4 +1,7 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; internal class Program { @@ -11,7 +14,46 @@ internal class Program builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + + + + + + builder.Services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Microser.API.Weather", Version = "v1" }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("https://localhost:5001/connect/authorize"), + TokenUrl = new Uri("https://localhost:5001/connect/token"), + Scopes = new Dictionary + { + {"openid", "OpenId - full access"}, + {"profile", "Profile - full access"}, + {"address", "Address - full access"}, + {"email", "Email - full access"}, + {"roles", "Your role(s) - full access"}, + {"scope1", "scope1 - full access"}, + {"microser_api_weather", "microser_api_weather - full access"}, + } + } + } + }); + + options.OperationFilter(); + }); + + + + + + builder.Services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => @@ -25,7 +67,7 @@ internal class Program builder.Services.AddAuthorization(options => { - options.AddPolicy("ClientIdPolicy", policy => policy.RequireClaim("client_id", "microser_api_weather", "dotnet_blazor_serverapp")); + options.AddPolicy("ClientIdPolicy", policy => policy.RequireClaim("client_id", "microser_api_weather", "dotnet_blazor_serverapp", "dotnet_api_swagger")); }); var app = builder.Build(); @@ -34,7 +76,17 @@ internal class Program if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + //app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Microser.API.Weather v1"); + + options.OAuthClientId("dotnet_api_swagger"); + options.OAuthClientSecret("76CD4B0FC93846F08395BF8994B86BC6"); + options.OAuthAppName("Microser.API.Weather - Swagger"); + options.OAuthUsePkce(); + }); + } app.UseHttpsRedirection(); @@ -47,4 +99,28 @@ internal class Program app.Run(); } +} + +public class AuthorizeCheckOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (hasAuthorize) + { + operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + [new OpenApiSecurityScheme {Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "oauth2"}}] + = new[] {"api1"} + } + }; + } + } } \ No newline at end of file diff --git a/src/Microser/Microser.ApiGateway/Microser.ApiGateway.csproj b/src/Microser/Microser.ApiGateway/Microser.ApiGateway.csproj new file mode 100644 index 0000000..7f32c96 --- /dev/null +++ b/src/Microser/Microser.ApiGateway/Microser.ApiGateway.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/Microser/Microser.ApiGateway/Program.cs b/src/Microser/Microser.ApiGateway/Program.cs new file mode 100644 index 0000000..1ec4f7b --- /dev/null +++ b/src/Microser/Microser.ApiGateway/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.IdentityModel.Tokens; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +internal class Program +{ + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + + var authenticationProviderKey = "IdentityApiKey"; + + builder.Services.AddAuthentication() + .AddJwtBearer(authenticationProviderKey, x => + { + x.Authority = "https://localhost:5001"; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false + }; + }); + + builder.Services.AddOcelot(builder.Configuration); + + var app = builder.Build(); + + app.UseOcelot().Wait(); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/Microser/Microser.ApiGateway/Properties/launchSettings.json b/src/Microser/Microser.ApiGateway/Properties/launchSettings.json new file mode 100644 index 0000000..0706227 --- /dev/null +++ b/src/Microser/Microser.ApiGateway/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:6501", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Microser/Microser.ApiGateway/appsettings.Development.json b/src/Microser/Microser.ApiGateway/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Microser/Microser.ApiGateway/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Microser/Microser.ApiGateway/appsettings.json b/src/Microser/Microser.ApiGateway/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Microser/Microser.ApiGateway/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Microser/Microser.ApiGateway/ocelot.json b/src/Microser/Microser.ApiGateway/ocelot.json new file mode 100644 index 0000000..898edbf --- /dev/null +++ b/src/Microser/Microser.ApiGateway/ocelot.json @@ -0,0 +1,38 @@ +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/weatherforecast", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 6001 + } + ], + "UpstreamPathTemplate": "/weatherforecast", + "UpstreamHttpMethod": [ "GET", "POST" ], + "AuthenticationOptions": { + "AuthenticationProviderKey": "IdentityApiKey", + "AllowedScopes": [] + } + }, + { + "DownstreamPathTemplate": "/api/weatherforecast/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 6001 + } + ], + "UpstreamPathTemplate": "/weatherforecast/{id}", + "UpstreamHttpMethod": [ "GET", "PUT", "DELETE" ], + "AuthenticationOptions": { + "AuthenticationProviderKey": "IdentityApiKey", + "AllowedScopes": [] + } + }, + + + ] +} diff --git a/src/Microser/Microser.BlazorAppClient/Program.cs b/src/Microser/Microser.BlazorAppClient/Program.cs index c5e7b2a..53200bd 100644 --- a/src/Microser/Microser.BlazorAppClient/Program.cs +++ b/src/Microser/Microser.BlazorAppClient/Program.cs @@ -126,13 +126,23 @@ public static class StartupExtensions public static void AddHttpClients(this IServiceCollection services) { - services.AddHttpClient("WeatherForecastAPIClient", client => + // connect API from ApiGateway + services.AddHttpClient("APIGateway", client => { - client.BaseAddress = new Uri("https://localhost:6001/"); + client.BaseAddress = new Uri("https://localhost:6501/"); client.DefaultRequestHeaders.Clear(); client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json"); }) - .AddHttpMessageHandler(); + .AddHttpMessageHandler(); + + ////directly connect to API + //services.AddHttpClient("WeatherForecastAPIClient", client => + //{ + // client.BaseAddress = new Uri("https://localhost:6001/"); + // client.DefaultRequestHeaders.Clear(); + // client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json"); + //}) + // .AddHttpMessageHandler(); // added for get user info services.AddHttpClient("IDPClient", client => diff --git a/src/Microser/Microser.BlazorAppClient/Services/WeatherForecastApiService.cs b/src/Microser/Microser.BlazorAppClient/Services/WeatherForecastApiService.cs index dccd252..6074cc5 100644 --- a/src/Microser/Microser.BlazorAppClient/Services/WeatherForecastApiService.cs +++ b/src/Microser/Microser.BlazorAppClient/Services/WeatherForecastApiService.cs @@ -16,7 +16,8 @@ public class WeatherForecastApiService : IWeatherForecastApiService public async Task GetAllAsync() { - var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + // get from gateway + var httpClient = _httpClientFactory.CreateClient("APIGateway"); var request = new HttpRequestMessage( HttpMethod.Get, "/WeatherForecast"); @@ -24,6 +25,15 @@ public class WeatherForecastApiService : IWeatherForecastApiService .ConfigureAwait(false); response.EnsureSuccessStatusCode(); + //// directly from API + //var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + //var request = new HttpRequestMessage( + // HttpMethod.Get, + // "/api/WeatherForecast"); + //var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) + // .ConfigureAwait(false); + //response.EnsureSuccessStatusCode(); + //var content = await response.Content.ReadAsStringAsync(); //var weatherForecastList = JsonSerializer.Deserialize>(content); @@ -74,7 +84,7 @@ public class WeatherForecastApiService : IWeatherForecastApiService public async Task GetByIdAsync(int id) { - var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + var httpClient = _httpClientFactory.CreateClient("APIGateway"); var request = new HttpRequestMessage( HttpMethod.Get, "/WeatherForecast/" + id); @@ -87,7 +97,7 @@ public class WeatherForecastApiService : IWeatherForecastApiService public async Task AddAsync(WeatherForecast weatherForecast) { - var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + var httpClient = _httpClientFactory.CreateClient("APIGateway"); var request = new HttpRequestMessage( HttpMethod.Post, "/WeatherForecast") { @@ -104,7 +114,7 @@ public class WeatherForecastApiService : IWeatherForecastApiService public async Task DeleteByIdAsync(int id) { - var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + var httpClient = _httpClientFactory.CreateClient("APIGateway"); var request = new HttpRequestMessage( HttpMethod.Delete, "/WeatherForecast/" + id.ToString()); @@ -124,7 +134,7 @@ public class WeatherForecastApiService : IWeatherForecastApiService public async Task UpdateAsync(int id, WeatherForecast weatherForecast) { - var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient"); + var httpClient = _httpClientFactory.CreateClient("APIGateway"); var request = new HttpRequestMessage( HttpMethod.Put, "/WeatherForecast/" + weatherForecast.Id.ToString()) { diff --git a/src/Microser/Microser.IdS/Config.cs b/src/Microser/Microser.IdS/Config.cs index 2f5f58c..2b3d0b8 100644 --- a/src/Microser/Microser.IdS/Config.cs +++ b/src/Microser/Microser.IdS/Config.cs @@ -56,6 +56,30 @@ namespace Microser.IdS Enabled = true }, + new Client + { + ClientId = "dotnet_api_swagger", + ClientName = "Dotnet Swagger UI Auth", + ClientSecrets = { + new Secret("76CD4B0FC93846F08395BF8994B86BC6".Sha256()) + }, + AllowedGrantTypes = GrantTypes.Code, + RequirePkce = true, + RequireClientSecret = true, + AllowedCorsOrigins = { "https://localhost:6001" }, + AllowedScopes = { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Address, + IdentityServerConstants.StandardScopes.Email, + "roles", + "scope1", + "microser_api_weather" + }, + RedirectUris = { "https://localhost:6001/swagger/oauth2-redirect.html" }, + Enabled = true + }, + // m2m client credentials flow client new Client { diff --git a/src/Microser/Microser.IdS/IdentityServer.db b/src/Microser/Microser.IdS/IdentityServer.db index e50be6b..6d8555c 100644 Binary files a/src/Microser/Microser.IdS/IdentityServer.db and b/src/Microser/Microser.IdS/IdentityServer.db differ diff --git a/src/Microser/Microser.IdS/IdentityServer.db-shm b/src/Microser/Microser.IdS/IdentityServer.db-shm index fa691ba..36fca90 100644 Binary files a/src/Microser/Microser.IdS/IdentityServer.db-shm and b/src/Microser/Microser.IdS/IdentityServer.db-shm differ diff --git a/src/Microser/Microser.IdS/IdentityServer.db-wal b/src/Microser/Microser.IdS/IdentityServer.db-wal index 0cd0c74..703523a 100644 Binary files a/src/Microser/Microser.IdS/IdentityServer.db-wal and b/src/Microser/Microser.IdS/IdentityServer.db-wal differ diff --git a/src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql b/src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql index f89a81a..b81c1e9 100644 --- a/src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql +++ b/src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql @@ -295,7 +295,7 @@ CREATE UNIQUE INDEX "IX_IdentityResourceProperties_IdentityResourceId_Key" ON "I CREATE UNIQUE INDEX "IX_IdentityResources_Name" ON "IdentityResources" ("Name"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20240312131641_Configuration', '8.0.0'); +VALUES ('20240316101904_Configuration', '8.0.0'); COMMIT; diff --git a/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs b/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.Designer.cs similarity index 99% rename from src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs rename to src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.Designer.cs index 6d07ecd..8311102 100644 --- a/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs +++ b/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microser.IdS.Migrations.ConfigurationDb { [DbContext(typeof(ConfigurationDbContext))] - [Migration("20240312131641_Configuration")] + [Migration("20240316101904_Configuration")] partial class Configuration { /// diff --git a/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.cs b/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.cs similarity index 99% rename from src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.cs rename to src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.cs index 491001b..b6c6de9 100644 --- a/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.cs +++ b/src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240316101904_Configuration.cs @@ -718,4 +718,4 @@ namespace Microser.IdS.Migrations.ConfigurationDb name: "IdentityResources"); } } -} \ No newline at end of file +} diff --git a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql index e0eede2..2e52da2 100644 --- a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql +++ b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql @@ -93,7 +93,7 @@ CREATE INDEX "IX_ServerSideSessions_SessionId" ON "ServerSideSessions" ("Session CREATE INDEX "IX_ServerSideSessions_SubjectId" ON "ServerSideSessions" ("SubjectId"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20240312131625_Grants', '8.0.0'); +VALUES ('20240316101846_Grants', '8.0.0'); COMMIT; diff --git a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.Designer.cs similarity index 99% rename from src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs rename to src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.Designer.cs index ef2b6bf..714bd66 100644 --- a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs +++ b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microser.IdS.Migrations.PersistedGrantDb { [DbContext(typeof(PersistedGrantDbContext))] - [Migration("20240312131625_Grants")] + [Migration("20240316101846_Grants")] partial class Grants { /// diff --git a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.cs b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.cs similarity index 99% rename from src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.cs rename to src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.cs index b0d19c0..c770ff1 100644 --- a/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.cs +++ b/src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240316101846_Grants.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -204,4 +205,4 @@ namespace Microser.IdS.Migrations.PersistedGrantDb name: "ServerSideSessions"); } } -} \ No newline at end of file +} diff --git a/src/Microser/Microser.sln b/src/Microser/Microser.sln index fd11074..b7d04e1 100644 --- a/src/Microser/Microser.sln +++ b/src/Microser/Microser.sln @@ -3,13 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34601.278 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microser.IdS", "Microser.IdS\Microser.IdS.csproj", "{28CA6FDC-65AA-47DA-BD42-F04B64D8E193}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microser.IdS", "Microser.IdS\Microser.IdS.csproj", "{28CA6FDC-65AA-47DA-BD42-F04B64D8E193}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microser.API.Weather", "Microser.API.Weather\Microser.API.Weather.csproj", "{DEDE691E-E9A2-4B39-A390-DC265B0EC164}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microser.API.Weather", "Microser.API.Weather\Microser.API.Weather.csproj", "{DEDE691E-E9A2-4B39-A390-DC265B0EC164}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microser.BlazorAppClient", "Microser.BlazorAppClient\Microser.BlazorAppClient.csproj", "{A96610DB-BA5D-47B6-B2AF-62D44EAEECD4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microser.BlazorAppClient", "Microser.BlazorAppClient\Microser.BlazorAppClient.csproj", "{A96610DB-BA5D-47B6-B2AF-62D44EAEECD4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microser.Core", "Microser.Core\Microser.Core.csproj", "{1805CA67-CDB9-45E5-A13A-4783DCBE5F51}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microser.Core", "Microser.Core\Microser.Core.csproj", "{1805CA67-CDB9-45E5-A13A-4783DCBE5F51}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microser.ApiGateway", "Microser.ApiGateway\Microser.ApiGateway.csproj", "{C9777697-B116-4A7D-84C3-DC73FEF03F57}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +35,10 @@ Global {1805CA67-CDB9-45E5-A13A-4783DCBE5F51}.Debug|Any CPU.Build.0 = Debug|Any CPU {1805CA67-CDB9-45E5-A13A-4783DCBE5F51}.Release|Any CPU.ActiveCfg = Release|Any CPU {1805CA67-CDB9-45E5-A13A-4783DCBE5F51}.Release|Any CPU.Build.0 = Release|Any CPU + {C9777697-B116-4A7D-84C3-DC73FEF03F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9777697-B116-4A7D-84C3-DC73FEF03F57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9777697-B116-4A7D-84C3-DC73FEF03F57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9777697-B116-4A7D-84C3-DC73FEF03F57}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE