From a35639881cd099f964a8c52b8ba7ec44ed99f729 Mon Sep 17 00:00:00 2001 From: Farshad DASHTI <78855469+FrostyApeOne@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:34:55 +0100 Subject: [PATCH] Added benchmarks Added a feature flag to enable/disable PerformanceBehaviour --- .gitignore | 1 + .../ConstituencyQueryHandlerBenchmark.cs | 119 +++++++ .../Dfe.PersonsApi.Benchmarks.csproj | 30 ++ .../appsettings.json | 5 + .../ApplicationServiceCollectionExtensions.cs | 8 +- PersonsApi/appsettings.Development.json | 3 + PersonsApi/appsettings.Production.json | 3 + PersonsApi/appsettings.Test.json | 3 + .../Helpers/DbContextHelper.cs | 320 ++++++++++-------- TramsDataApi.sln | 9 +- 10 files changed, 360 insertions(+), 141 deletions(-) create mode 100644 Benchmarks/Dfe.PersonsApi.Benchmarks/ConstituencyQueryHandlerBenchmark.cs create mode 100644 Benchmarks/Dfe.PersonsApi.Benchmarks/Dfe.PersonsApi.Benchmarks.csproj create mode 100644 Benchmarks/Dfe.PersonsApi.Benchmarks/appsettings.json diff --git a/.gitignore b/.gitignore index 418ffd986..d2b4f7969 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ terraform.rc* !terraform.tfvars.example .terraform/ backend.vars +/Benchmarks/Dfe.PersonsApi.Benchmarks/BenchmarkDotNet.Artifacts/results diff --git a/Benchmarks/Dfe.PersonsApi.Benchmarks/ConstituencyQueryHandlerBenchmark.cs b/Benchmarks/Dfe.PersonsApi.Benchmarks/ConstituencyQueryHandlerBenchmark.cs new file mode 100644 index 000000000..a16966115 --- /dev/null +++ b/Benchmarks/Dfe.PersonsApi.Benchmarks/ConstituencyQueryHandlerBenchmark.cs @@ -0,0 +1,119 @@ +using AutoMapper; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using Dfe.Academies.Application.Common.Behaviours; +using Dfe.Academies.Application.Constituencies.Queries.GetMemberOfParliamentByConstituencies; +using Dfe.Academies.Application.Constituencies.Queries.GetMemberOfParliamentByConstituency; +using Dfe.Academies.Application.MappingProfiles; +using Dfe.Academies.Domain.Interfaces.Caching; +using Dfe.Academies.Domain.Interfaces.Repositories; +using Dfe.Academies.Infrastructure; +using Dfe.Academies.Infrastructure.Caching; +using Dfe.Academies.Infrastructure.Repositories; +using Dfe.Academies.Testing.Common.Helpers; +using Dfe.Academies.Utils.Caching; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Dfe.PersonsApi.Benchmarks +{ + [MemoryDiagnoser] + [SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 10, invocationCount: 50)] + public class ConstituencyQueryHandlerBenchmark + { + [Params("Test Constituency 1")] + public string? ConstituencyName; + + [Params(true, false)] + public bool IncludePerformanceBehaviour; + + private IMediator? _mediator; + private GetMembersOfParliamentByConstituenciesQuery? _query; + private IConstituencyRepository? _realRepository; + private ICacheService? _cacheService; + private IMapper? _mapper; + + [GlobalSetup] + public void Setup() + { + var services = new ServiceCollection(); + + var dbContext = DbContextHelper.CreateDbContext(services); + _realRepository = new ConstituencyRepository(dbContext); + + var config = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + }); + _mapper = config.CreateMapper(); + + var httpContextAccessor = new HttpContextAccessor + { + HttpContext = new DefaultHttpContext() + }; + + var memoryCache = new MemoryCache(new MemoryCacheOptions()); + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + var logger = loggerFactory.CreateLogger(); + + var cacheSettings = Options.Create(new CacheSettings + { + DefaultDurationInSeconds = 600, // 10 minutes + Durations = new Dictionary + { + { nameof(GetMemberOfParliamentByConstituencyQueryHandler), 300 } // 5 minutes + } + }); + + _cacheService = new MemoryCacheService(memoryCache, logger, cacheSettings); + + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssembly(typeof(GetMemberOfParliamentByConstituencyQueryHandler).Assembly); + + if (IncludePerformanceBehaviour) + { + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); + } + }); + + services.AddSingleton(loggerFactory); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + services.AddSingleton(_realRepository); + services.AddSingleton(_cacheService); + services.AddSingleton(_mapper); + services.AddSingleton(httpContextAccessor); + + var provider = services.BuildServiceProvider(); + _mediator = provider.GetRequiredService(); + + _query = new GetMembersOfParliamentByConstituenciesQuery(dbContext.Constituencies.Select(x => x.ConstituencyName).Take(100).ToList()); + } + + [Benchmark] + public async Task RunHandlerWithCacheAsync() + { + await _mediator?.Send(_query!)!; + } + + [Benchmark] + public async Task RunHandlerWithoutCacheAsync() + { + var cacheKey = $"MemberOfParliament_{CacheKeyHelper.GenerateHashedCacheKey(_query?.ConstituencyNames!)}"; + _cacheService?.Remove(cacheKey); + await _mediator?.Send(_query!)!; + } + + public static class Program + { + public static void Main(string[] args) + { + BenchmarkRunner.Run(); + } + } + } +} diff --git a/Benchmarks/Dfe.PersonsApi.Benchmarks/Dfe.PersonsApi.Benchmarks.csproj b/Benchmarks/Dfe.PersonsApi.Benchmarks/Dfe.PersonsApi.Benchmarks.csproj new file mode 100644 index 000000000..d42e922b5 --- /dev/null +++ b/Benchmarks/Dfe.PersonsApi.Benchmarks/Dfe.PersonsApi.Benchmarks.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + Exe + true + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/Benchmarks/Dfe.PersonsApi.Benchmarks/appsettings.json b/Benchmarks/Dfe.PersonsApi.Benchmarks/appsettings.json new file mode 100644 index 000000000..aedbd8c19 --- /dev/null +++ b/Benchmarks/Dfe.PersonsApi.Benchmarks/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=sip;User ID=sa;Password=StrongPassword905;TrustServerCertificate=True" + } +} \ No newline at end of file diff --git a/Dfe.Academies.Application/ApplicationServiceCollectionExtensions.cs b/Dfe.Academies.Application/ApplicationServiceCollectionExtensions.cs index b25294557..d15690807 100644 --- a/Dfe.Academies.Application/ApplicationServiceCollectionExtensions.cs +++ b/Dfe.Academies.Application/ApplicationServiceCollectionExtensions.cs @@ -26,6 +26,8 @@ public static IServiceCollection AddApplicationDependencyGroup( public static IServiceCollection AddPersonsApiApplicationDependencyGroup( this IServiceCollection services, IConfiguration config) { + var performanceLoggingEnabled = config.GetValue("Features:PerformanceLoggingEnabled"); + services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); services.AddMediatR(cfg => @@ -33,7 +35,11 @@ public static IServiceCollection AddPersonsApiApplicationDependencyGroup( cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); - cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); + + if (performanceLoggingEnabled) + { + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); + } }); return services; diff --git a/PersonsApi/appsettings.Development.json b/PersonsApi/appsettings.Development.json index d856d0f9c..c1db5df0c 100644 --- a/PersonsApi/appsettings.Development.json +++ b/PersonsApi/appsettings.Development.json @@ -28,5 +28,8 @@ "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", "ClientId": "930a077f-43d0-48cb-9316-1e0430eeaf6b", "Audience": "api://930a077f-43d0-48cb-9316-1e0430eeaf6b" + }, + "Features": { + "PerformanceLoggingEnabled": true } } \ No newline at end of file diff --git a/PersonsApi/appsettings.Production.json b/PersonsApi/appsettings.Production.json index bb40eab6c..93a0e6e99 100644 --- a/PersonsApi/appsettings.Production.json +++ b/PersonsApi/appsettings.Production.json @@ -19,5 +19,8 @@ "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", "ClientId": "abae81d8-c7e3-4f28-8349-c43f554a712b", "Audience": "api://abae81d8-c7e3-4f28-8349-c43f554a712b" + }, + "Features": { + "PerformanceLoggingEnabled": false } } \ No newline at end of file diff --git a/PersonsApi/appsettings.Test.json b/PersonsApi/appsettings.Test.json index e2c29bd03..c147cb1c9 100644 --- a/PersonsApi/appsettings.Test.json +++ b/PersonsApi/appsettings.Test.json @@ -25,5 +25,8 @@ "TenantId": "9c7d9dd3-840c-4b3f-818e-552865082e16", "ClientId": "930a077f-43d0-48cb-9316-1e0430eeaf6b", "Audience": "api://930a077f-43d0-48cb-9316-1e0430eeaf6b" + }, + "Features": { + "PerformanceLoggingEnabled": true } } \ No newline at end of file diff --git a/Tests/Dfe.Academies.Testing.Common/Helpers/DbContextHelper.cs b/Tests/Dfe.Academies.Testing.Common/Helpers/DbContextHelper.cs index 4b8bee38e..974173289 100644 --- a/Tests/Dfe.Academies.Testing.Common/Helpers/DbContextHelper.cs +++ b/Tests/Dfe.Academies.Testing.Common/Helpers/DbContextHelper.cs @@ -1,12 +1,13 @@ -using Dfe.Academies.Domain.Constituencies; +using System.Data.Common; +using Dfe.Academies.Domain.Constituencies; using Dfe.Academies.Domain.Establishment; using Dfe.Academies.Domain.Trust; using Dfe.Academies.Domain.ValueObjects; using Dfe.Academies.Infrastructure; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using System.Data.Common; namespace Dfe.Academies.Testing.Common.Helpers { @@ -14,16 +15,27 @@ public static class DbContextHelper where TContext : DbContext { public static TContext CreateDbContext(IServiceCollection services) { - var connection = new SqliteConnection("DataSource=:memory:"); - connection.Open(); + var connectionString = GetConnectionStringFromConfig(); - services.AddSingleton(_ => connection); + if (string.IsNullOrEmpty(connectionString) || connectionString.Contains("DataSource=:memory:")) + { + var connection = new SqliteConnection(connectionString); + connection.Open(); - services.AddDbContext((sp, options) => + services.AddSingleton(_ => connection); + services.AddDbContext((sp, options) => + { + var conn = sp.GetRequiredService(); + options.UseSqlite(conn); + }); + } + else { - var conn = sp.GetRequiredService(); - options.UseSqlite(conn); - }); + services.AddDbContext(options => + { + options.UseSqlServer(connectionString); + }); + } var serviceProvider = services.BuildServiceProvider(); var dbContext = serviceProvider.GetRequiredService(); @@ -38,144 +50,174 @@ private static void SeedTestData(TContext context) { if (context is MstrContext mstrContext) { - // Populate Trust - var trust1 = new Trust { SK = 1, Name = "Trust A", TrustTypeId = mstrContext.TrustTypes.FirstOrDefault()?.SK, GroupUID = "G1", Modified = DateTime.UtcNow, ModifiedBy = "System" }; - var trust2 = new Trust { SK = 2, Name = "Trust B", TrustTypeId = mstrContext.TrustTypes.FirstOrDefault()?.SK, GroupUID = "G2", Modified = DateTime.UtcNow, ModifiedBy = "System" }; - mstrContext.Trusts.AddRange(trust1, trust2); - - // Populate Establishment - var establishment1 = new Establishment - { - SK = 1, - EstablishmentName = "School A", - LocalAuthorityId = mstrContext.LocalAuthorities.FirstOrDefault()?.SK, - EstablishmentTypeId = mstrContext.EstablishmentTypes.FirstOrDefault()?.SK, - Latitude = 54.9784, - Longitude = -1.6174, - MainPhone = "01234567890", - Email = "schoolA@example.com", - Modified = DateTime.UtcNow, - ModifiedBy = "System" - }; - var establishment2 = new Establishment - { - SK = 2, - EstablishmentName = "School B", - LocalAuthorityId = mstrContext.LocalAuthorities.FirstOrDefault()?.SK, - EstablishmentTypeId = mstrContext.EstablishmentTypes.FirstOrDefault()?.SK, - Latitude = 50.3763, - Longitude = -4.1427, - MainPhone = "09876543210", - Email = "schoolB@example.com", - Modified = DateTime.UtcNow, - ModifiedBy = "System" - }; - mstrContext.Establishments.AddRange(establishment1, establishment2); - - // Populate EducationEstablishmentTrust - var educationEstablishmentTrust1 = new EducationEstablishmentTrust - { - SK = 1, - EducationEstablishmentId = (int)establishment1.SK, - TrustId = (int)trust1.SK, - }; - var educationEstablishmentTrust2 = new EducationEstablishmentTrust + if (!mstrContext.Trusts.Any() && !mstrContext.Establishments.Any() && + !mstrContext.EducationEstablishmentTrusts.Any() && !mstrContext.GovernanceRoleTypes.Any() && + !mstrContext.EducationEstablishmentGovernances.Any()) { - SK = 2, - EducationEstablishmentId = (int)establishment2.SK, - TrustId = (int)trust2.SK, - }; - mstrContext.EducationEstablishmentTrusts.AddRange(educationEstablishmentTrust1, educationEstablishmentTrust2); - - // Populate GovernanceRoleType - var governanceRoleType1 = new GovernanceRoleType { SK = 1, Name = "Chair of Governors", Modified = DateTime.UtcNow, ModifiedBy = "System" }; - var governanceRoleType2 = new GovernanceRoleType { SK = 2, Name = "Vice Chair of Governors", Modified = DateTime.UtcNow, ModifiedBy = "System" }; - mstrContext.GovernanceRoleTypes.AddRange(governanceRoleType1, governanceRoleType2); - - // Populate EducationEstablishmentGovernance - var governance1 = new EducationEstablishmentGovernance - { - SK = 1, - EducationEstablishmentId = establishment1.SK, - GovernanceRoleTypeId = governanceRoleType1.SK, - GID = "GID1", - Title = "Mr.", - Forename1 = "John", - Surname = "Doe", - Email = "johndoe@example.com", - Modified = DateTime.UtcNow, - ModifiedBy = "System" - }; - var governance3 = new EducationEstablishmentGovernance - { - SK = 3, - EducationEstablishmentId = establishment1.SK, - GovernanceRoleTypeId = governanceRoleType2.SK, - GID = "GID2", - Title = "Ms.", - Forename1 = "Anna", - Surname = "Smith", - Email = "annasmith@example.com", - Modified = DateTime.UtcNow, - ModifiedBy = "System" - }; - mstrContext.EducationEstablishmentGovernances.AddRange(governance1, governance3); - - // Save changes - mstrContext.SaveChanges(); + // Populate Trust + var trust1 = new Trust + { + SK = 1, Name = "Trust A", TrustTypeId = mstrContext.TrustTypes.FirstOrDefault()?.SK, + GroupUID = "G1", Modified = DateTime.UtcNow, ModifiedBy = "System" + }; + var trust2 = new Trust + { + SK = 2, Name = "Trust B", TrustTypeId = mstrContext.TrustTypes.FirstOrDefault()?.SK, + GroupUID = "G2", Modified = DateTime.UtcNow, ModifiedBy = "System" + }; + mstrContext.Trusts.AddRange(trust1, trust2); + + // Populate Establishment + var establishment1 = new Establishment + { + SK = 1, + EstablishmentName = "School A", + LocalAuthorityId = mstrContext.LocalAuthorities.FirstOrDefault()?.SK, + EstablishmentTypeId = mstrContext.EstablishmentTypes.FirstOrDefault()?.SK, + Latitude = 54.9784, + Longitude = -1.6174, + MainPhone = "01234567890", + Email = "schoolA@example.com", + Modified = DateTime.UtcNow, + ModifiedBy = "System" + }; + var establishment2 = new Establishment + { + SK = 2, + EstablishmentName = "School B", + LocalAuthorityId = mstrContext.LocalAuthorities.FirstOrDefault()?.SK, + EstablishmentTypeId = mstrContext.EstablishmentTypes.FirstOrDefault()?.SK, + Latitude = 50.3763, + Longitude = -4.1427, + MainPhone = "09876543210", + Email = "schoolB@example.com", + Modified = DateTime.UtcNow, + ModifiedBy = "System" + }; + mstrContext.Establishments.AddRange(establishment1, establishment2); + + // Populate EducationEstablishmentTrust + var educationEstablishmentTrust1 = new EducationEstablishmentTrust + { + SK = 1, + EducationEstablishmentId = (int)establishment1.SK, + TrustId = (int)trust1.SK, + }; + var educationEstablishmentTrust2 = new EducationEstablishmentTrust + { + SK = 2, + EducationEstablishmentId = (int)establishment2.SK, + TrustId = (int)trust2.SK, + + }; + mstrContext.EducationEstablishmentTrusts.AddRange(educationEstablishmentTrust1, + educationEstablishmentTrust2); + + // Populate GovernanceRoleType + var governanceRoleType1 = new GovernanceRoleType + { SK = 1, Name = "Chair of Governors", Modified = DateTime.UtcNow, ModifiedBy = "System" }; + var governanceRoleType2 = new GovernanceRoleType + { SK = 2, Name = "Vice Chair of Governors", Modified = DateTime.UtcNow, ModifiedBy = "System" }; + mstrContext.GovernanceRoleTypes.AddRange(governanceRoleType1, governanceRoleType2); + + // Populate EducationEstablishmentGovernance + var governance1 = new EducationEstablishmentGovernance + { + SK = 1, + EducationEstablishmentId = establishment1.SK, + GovernanceRoleTypeId = governanceRoleType1.SK, + GID = "GID1", + Title = "Mr.", + Forename1 = "John", + Surname = "Doe", + Email = "johndoe@example.com", + Modified = DateTime.UtcNow, + ModifiedBy = "System" + }; + var governance3 = new EducationEstablishmentGovernance + { + SK = 3, + EducationEstablishmentId = establishment1.SK, + GovernanceRoleTypeId = governanceRoleType2.SK, + GID = "GID2", + Title = "Ms.", + Forename1 = "Anna", + Surname = "Smith", + Email = "annasmith@example.com", + Modified = DateTime.UtcNow, + ModifiedBy = "System" + }; + mstrContext.EducationEstablishmentGovernances.AddRange(governance1, governance3); + + // Save changes + mstrContext.SaveChanges(); + } } if (context is MopContext mopContext) { - var memberContact1 = new MemberContactDetails( - new MemberId(1), - 1, - "test1@example.com", - null - ); - - var memberContact2 = new MemberContactDetails( - new MemberId(2), - 1, - "test2@example.com", - null - ); - - var constituency1 = new Constituency( - new ConstituencyId(1), - new MemberId(1), - "Test Constituency 1", - new NameDetails( - "Wood, John", - "John Wood", - "Mr. John Wood MP" - ), - DateTime.UtcNow, - null, - memberContact1 - ); - - var constituency2 = new Constituency( - new ConstituencyId(2), - new MemberId(2), - "Test Constituency 2", - new NameDetails( - "Wood, Joe", - "Joe Wood", - "Mr. Joe Wood MP" - ), - DateTime.UtcNow, - null, - memberContact2 - ); - - mopContext.Constituencies.Add(constituency1); - mopContext.Constituencies.Add(constituency2); - - mopContext.SaveChanges(); + if (!mopContext.Constituencies.Any()) + { + + var memberContact1 = new MemberContactDetails( + new MemberId(1), + 1, + "test1@example.com", + null + ); + + var memberContact2 = new MemberContactDetails( + new MemberId(2), + 1, + "test2@example.com", + null + ); + + var constituency1 = new Constituency( + new ConstituencyId(1), + new MemberId(1), + "Test Constituency 1", + new NameDetails( + "Wood, John", + "John Wood", + "Mr. John Wood MP" + ), + DateTime.UtcNow, + null, + memberContact1 + ); + + var constituency2 = new Constituency( + new ConstituencyId(2), + new MemberId(2), + "Test Constituency 2", + new NameDetails( + "Wood, Joe", + "Joe Wood", + "Mr. Joe Wood MP" + ), + DateTime.UtcNow, + null, + memberContact2 + ); + + mopContext.Constituencies.Add(constituency1); + mopContext.Constituencies.Add(constituency2); + + mopContext.SaveChanges(); + } } } + private static string? GetConnectionStringFromConfig() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); + + return configuration.GetConnectionString("DefaultConnection"); + } } } diff --git a/TramsDataApi.sln b/TramsDataApi.sln index d894b89e9..2bd9a4308 100644 --- a/TramsDataApi.sln +++ b/TramsDataApi.sln @@ -30,7 +30,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.PersonsApi.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.Testing.Common", "Tests\Dfe.Academies.Testing.Common\Dfe.Academies.Testing.Common.csproj", "{777C300F-FBB1-402A-A850-1D26417FA412}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dfe.Academies.Domain.Tests", "Tests\Dfe.Academies.Domain.Tests\Dfe.Academies.Domain.Tests.csproj", "{82966208-86EA-4459-8D99-FED765D07D82}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Academies.Domain.Tests", "Tests\Dfe.Academies.Domain.Tests\Dfe.Academies.Domain.Tests.csproj", "{82966208-86EA-4459-8D99-FED765D07D82}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{CD758992-0CA5-4FBD-B171-0D574322C40E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.PersonsApi.Benchmarks", "Benchmarks\Dfe.PersonsApi.Benchmarks\Dfe.PersonsApi.Benchmarks.csproj", "{419DD205-E174-41BC-9C11-525308CEEA2B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -82,6 +86,8 @@ Global {82966208-86EA-4459-8D99-FED765D07D82}.Debug|Any CPU.Build.0 = Debug|Any CPU {82966208-86EA-4459-8D99-FED765D07D82}.Release|Any CPU.ActiveCfg = Release|Any CPU {82966208-86EA-4459-8D99-FED765D07D82}.Release|Any CPU.Build.0 = Release|Any CPU + {419DD205-E174-41BC-9C11-525308CEEA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {419DD205-E174-41BC-9C11-525308CEEA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +97,7 @@ Global {EB0D20C1-4818-44DF-97A7-276C9F96CC64} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} {777C300F-FBB1-402A-A850-1D26417FA412} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} {82966208-86EA-4459-8D99-FED765D07D82} = {08B2EC51-B7D3-4D6E-BF99-C2ADC957F387} + {419DD205-E174-41BC-9C11-525308CEEA2B} = {CD758992-0CA5-4FBD-B171-0D574322C40E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F0704299-A9C2-448A-B816-E5BCCB345AF8}