v13: New Backoffice in CMS (POC) (#13664)

* Added a new executeable to new backoffice

* add new backoffice client as submodule

* add new backoffice client as project

* add bootstrap of backoffice client

* experimentally allow CORS from local vite app running the backoffice

* fix base path

* move new backoffice projects to NewBackoffice folder

* add support for redirect urls to login page (temporary)

* update references to v13

* override databaseinstall/index.cshtml

* copy ignore lines from normal project

* remove redirect to AuthorizeUpgrade

* codeql: checkout submodules

* Section catch-all route

* fixed tests

* remove starter-kit file

* remove grid views

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Jacob Overgaard
2023-01-18 15:02:59 +01:00
committed by GitHub
parent 4640a317b8
commit 9c00c95f01
22 changed files with 542 additions and 20 deletions

View File

@@ -37,6 +37,7 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/Umbraco.Web.UI.New.Client"]
path = src/Umbraco.Web.UI.New.Client
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git

View File

@@ -27,8 +27,8 @@
</Target>
<Target Name="BuildBelle">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.New.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.New.Client\" Command="npm run build:for:cms" />
</Target>
<Target Name="CleanBellePreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">

View File

@@ -118,19 +118,6 @@ public class BackOfficeController : UmbracoController
[AllowAnonymous]
public async Task<IActionResult> Default()
{
// Check if we not are in an run state, if so we need to redirect
if (_runtimeState.Level != RuntimeLevel.Run)
{
if (_runtimeState.Level == RuntimeLevel.Upgrade)
{
return RedirectToAction(nameof(AuthorizeUpgrade), routeValues: new RouteValueDictionary()
{
["redir"] = _globalSettings.GetBackOfficePath(_hostingEnvironment),
});
}
return Redirect("/");
}
// force authentication to occur since this is not an authorized endpoint
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
@@ -227,7 +214,7 @@ public class BackOfficeController : UmbracoController
return new LocalRedirectResult(installerUrl);
}
var viewPath = Path.Combine(Constants.SystemDirectories.Umbraco, Constants.Web.Mvc.BackOfficeArea, nameof(AuthorizeUpgrade) + ".cshtml");
var viewPath = Path.Combine(Constants.SystemDirectories.Umbraco, Constants.Web.Mvc.BackOfficeArea, nameof(Default) + ".cshtml");
return await RenderDefaultOrProcessExternalLoginAsync(
result,

View File

@@ -1,3 +1,6 @@
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -42,6 +45,8 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes
/// <inheritdoc />
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
switch (_runtimeState.Level)
{
case RuntimeLevel.Install:
@@ -75,6 +80,16 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes
new { action = @"[a-zA-Z]*", id = @"[a-zA-Z]*" });
endpoints.MapUmbracoApiRoute<AuthenticationController>(_umbracoPathSegment, Constants.Web.Mvc.BackOfficeApiArea, true, string.Empty);
endpoints.MapAreaControllerRoute(
"catch-all-sections-to-client",
Constants.Web.Mvc.BackOfficeArea,
new StringBuilder(_umbracoPathSegment).Append("/section/{**slug}").ToString(),
new
{
Controller = ControllerExtensions.GetControllerName<BackOfficeController>(),
Action = nameof(BackOfficeController.Default)
});
}
/// <summary>

12
src/Umbraco.Web.UI.New/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
umbraco/[Dd]ata/
umbraco/[Ll]ogs/
umbraco/[Mm]odels/
Views/
!Views/Partials/blocklist/
!Views/Partials/grid/
!Views/_ViewImports.cshtml
appsettings.json
appsettings.Development.json
appsettings.Local.json
appsettings-schema.json
appsettings-schema.*.json

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace Umbraco.Cms.Web.UI;
[ApiExplorerSettings(IgnoreApi=true)]
[Route("/umbraco/login")]
public class BackOfficeLoginController : Controller
{
// GET
public IActionResult Index()
{
return View("/umbraco/UmbracoLogin/Index.cshtml");
}
}

View File

@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Website.Controllers;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Web.UI.Composers
{
/// <summary>
/// Adds controllers to the service collection.
/// </summary>
/// <remarks>
/// <para>
/// Umbraco 9 out of the box, makes use of <see cref="DefaultControllerActivator"/> which doesn't resolve controller
/// instances from the IOC container, instead it resolves the required dependencies of the controller and constructs an instance
/// of the controller.
/// </para>
/// <para>
/// Some users may wish to switch to <see cref="ServiceBasedControllerActivator"/> (perhaps to make use of interception/decoration).
/// </para>
/// <para>
/// This composer exists to help us detect ambiguous constructors in the CMS such that we do not cause unnecessary effort downstream.
/// </para>
/// <para>
/// This Composer is not shipped by the Umbraco.Templates package.
/// </para>
/// </remarks>
public class ControllersAsServicesComposer : IComposer
{
/// <inheritdoc />
public void Compose(IUmbracoBuilder builder) => builder.Services
.AddMvc()
.AddControllersAsServicesWithoutChangingActivator();
}
internal static class MvcBuilderExtensions
{
/// <summary>
/// <see cref="MvcCoreMvcBuilderExtensions.AddControllersAsServices"/> but without the replacement of
/// <see cref="DefaultControllerActivator"/>.
/// </summary>
/// <remarks>
/// We don't need to opt in to <see cref="ServiceBasedControllerActivator"/> to ensure container validation
/// passes.
/// </remarks>
public static IMvcBuilder AddControllersAsServicesWithoutChangingActivator(this IMvcBuilder builder)
{
var feature = new ControllerFeature();
builder.PartManager.PopulateFeature(feature);
foreach (Type controller in feature.Controllers.Select(c => c.AsType()))
{
builder.Services.TryAddTransient(controller, controller);
}
builder.Services.AddUnique<RenderNoContentController>(x => new RenderNoContentController(x.GetService<IUmbracoContextAccessor>()!, x.GetService<IOptionsSnapshot<GlobalSettings>>()!, x.GetService<IHostingEnvironment>()!));
return builder;
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Umbraco.Cms.Web.UI
{
public class Program
{
public static void Main(string[] args)
=> CreateHostBuilder(args)
.Build()
.Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureUmbracoDefaults()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStaticWebAssets();
webBuilder.UseStartup<Startup>();
});
}
}

View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:11000",
"sslPort": 44339
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Umbraco.Web.UI": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:44339;http://localhost:11000"
}
}
}

View File

@@ -0,0 +1,81 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.UI
{
public class Startup
{
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _config;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="webHostEnvironment">The web hosting environment.</param>
/// <param name="config">The configuration.</param>
/// <remarks>
/// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337.
/// </remarks>
public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config)
{
_env = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Configures the services.
/// </summary>
/// <param name="services">The services.</param>
/// <remarks>
/// This method gets called by the runtime. Use this method to add services to the container.
/// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940.
/// </remarks>
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.Build();
}
/// <summary>
/// Configures the application.
/// </summary>
/// <param name="app">The application builder.</param>
/// <param name="env">The web hosting environment.</param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
Console.WriteLine(@"Enabling CORS in development mode");
app.UseCors(u =>
{
u.AllowCredentials();
u.AllowAnyMethod();
u.AllowAnyHeader();
u.WithOrigins(new[] { "http://127.0.0.1:5173", "http://localhost:5173" });
});
}
#if (UseHttpsRedirect)
app.UseHttpsRedirection();
#endif
app.UseUmbraco()
.WithMiddleware(u =>
{
u.UseBackOffice();
u.UseWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
}
}
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<RootNamespace>Umbraco.Cms.Web.UI.New</RootNamespace>
<EnablePackageValidation>false</EnablePackageValidation>
</PropertyGroup>
<Import Project="..\Umbraco.Cms.Targets\buildTransitive\Umbraco.Cms.Targets.props" />
<Import Project="..\Umbraco.Cms.Targets\buildTransitive\Umbraco.Cms.Targets.targets" />
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms\Umbraco.Cms.csproj" />
<ProjectReference Include="..\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Opt-in to app-local ICU to ensure consistent globalization APIs across different platforms -->
<PackageReference Include="Microsoft.ICU.ICU4C.Runtime" Version="68.2.0.9" />
<RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="68.2.0.9" Condition="$(RuntimeIdentifier.StartsWith('linux')) or $(RuntimeIdentifier.StartsWith('win')) or ('$(RuntimeIdentifier)' == '' and !$([MSBuild]::IsOSPlatform('osx')))" />
</ItemGroup>
<PropertyGroup>
<!-- Razor files are needed for the backoffice to work correctly -->
<CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>
<PropertyGroup>
<!-- Remove RazorCompileOnBuild and RazorCompileOnPublish when not using ModelsMode InMemoryAuto -->
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="umbraco\UmbracoInstall\Index.cshtml" />
</ItemGroup>
<Target Name="CopyAppsettingsTemplate" BeforeTargets="Build" Condition="!Exists('appsettings.json')">
<Message Text="Copying appsettings.template.json to appsettings.json because it doesn't exist" Importance="high" />
<Copy SourceFiles="appsettings.template.json" DestinationFiles="appsettings.json" />
</Target>
<Target Name="CopyAppsettingsDevelopmentTemplate" BeforeTargets="Build" Condition="!Exists('appsettings.Development.json')">
<Message Text="Copying appsettings.Development.template.json to appsettings.Development.json because it doesn't exist" Importance="high" />
<Copy SourceFiles="appsettings.Development.template.json" DestinationFiles="appsettings.Development.json" />
</Target>
</Project>

View File

@@ -0,0 +1,13 @@
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<Umbraco.Cms.Core.Models.Blocks.BlockListModel>
@{
if (Model?.Any() != true) { return; }
}
<div class="umb-block-list">
@foreach (var block in Model)
{
if (block?.ContentUdi == null) { continue; }
var data = block.Content;
@await Html.PartialAsync("blocklist/Components/" + data.ContentType.Alias, block)
}
</div>

View File

@@ -0,0 +1,47 @@
{
"$schema" : "./appsettings-schema.json",
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Examine.Lucene.Providers.LuceneIndex": "Debug",
"Examine.BaseIndexProvider": "Debug",
"Examine.Lucene.LoggingReplicationClient": "Debug",
"Examine.Lucene.ExamineReplicator": "Debug"
}
},
"WriteTo": [
{
"Name": "Async",
"Args": {
"configure": [
{
"Name": "Console"
}
]
}
}
]
},
"Umbraco": {
"CMS": {
"Examine": {
"LuceneDirectoryFactory": "TempFileSystemDirectoryFactory"
},
"Global": {
"Smtp": {
//"From": "your@email.here",
//"Host": "localhost",
// "Port": "25"
}
},
"Hosting": {
"Debug": true
},
"RuntimeMinification": {
"useInMemoryCache": true,
"cacheBuster": "Timestamp"
}
}
}
}

View File

@@ -0,0 +1,74 @@
{
"$schema": "./appsettings-schema.json",
"ConnectionStrings": {
"umbracoDbDSN": ""
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
}
},
"Umbraco": {
"CMS": {
"Content": {
"Notifications": {
"Email": "your@email.here"
},
"MacroErrors": "Throw"
},
"Global": {
"DefaultUILanguage": "en-us",
"HideTopLevelNodeFromPath": true,
"TimeOutInMinutes": 20,
"UseHttps": false
},
"Hosting": {
"Debug": false
},
"KeepAlive": {
"DisableKeepAliveTask": false,
"KeepAlivePingUrl": "~/api/keepalive/ping"
},
"RequestHandler": {
"ConvertUrlsToAscii": "try"
},
"RuntimeMinification": {
"dataFolder": "umbraco/Data/TEMP/Smidge",
"version": "637642136775050602"
},
"Security": {
"KeepUserLoggedIn": false,
"UsernameIsEmail": true,
"HideDisabledUsersInBackoffice": false,
"AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\",
"UserPassword": {
"RequiredLength": 10,
"RequireNonLetterOrDigit": false,
"RequireDigit": false,
"RequireLowercase": false,
"RequireUppercase": false,
"MaxFailedAccessAttemptsBeforeLockout": 5
},
"MemberPassword": {
"RequiredLength": 10,
"RequireNonLetterOrDigit": false,
"RequireDigit": false,
"RequireLowercase": false,
"RequireUppercase": false,
"MaxFailedAccessAttemptsBeforeLockout": 5
}
},
"Tours": {
"EnableTours": true
},
"ModelsBuilder": {
"ModelsMode": "InMemoryAuto"
}
}
}
}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/umbraco/" />
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/umbraco/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco</title>
<link rel="stylesheet" href="/umbraco/style.css"/>
<script type="module" src="/umbraco/main.js"></script>
</head>
<body class="uui-font uui-text" style="margin: 0; padding: 0; overflow: hidden">
<umb-app></umb-app>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/umbraco/" />
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/umbraco/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco</title>
<link rel="stylesheet" href="/umbraco/style.css"/>
<script type="module" src="/umbraco/main.js"></script>
</head>
<body class="uui-font uui-text" style="margin: 0; padding: 0; overflow: hidden">
<umb-app></umb-app>
</body>
</html>

View File

@@ -0,0 +1,54 @@
@{
string returnTo = "/umbraco";
var redirectToValues = Context.Request.Query["redirectTo"];
if (redirectToValues.Count > 0)
{
var redirectTo = redirectToValues[0];
if (redirectTo is not null)
{
returnTo = redirectTo;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Umbraco</title>
<link rel="stylesheet" href="https://unpkg.com/blocks.css/dist/blocks.min.css" />
<style>
body {
display: grid;
place-content: center;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
}
.card {
display: grid;
place-items: center;
}
</style>
</head>
<body>
<div class="card fixed block">
<h1>Umbraco login universe</h1>
<p>We have not yet implemented login, so for your pleasure, we present you a login button that just works™</p>
<button type="button" id="login" class="accent block">LOGIN</button>
</div>
<script>
!(function () {
const button = document.getElementById("login");
button.addEventListener('click', () => {
sessionStorage.setItem('is-authenticated', 'true');
location.href = '@returnTo';
});
})();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -47,11 +47,11 @@ public class BackOfficeAreaRoutesTests
Assert.AreEqual(1, endpoints.DataSources.Count);
var route = endpoints.DataSources.First();
Assert.AreEqual(3, route.Endpoints.Count);
Assert.AreEqual(4, route.Endpoints.Count);
AssertMinimalBackOfficeRoutes(route);
var endpoint4 = (RouteEndpoint)route.Endpoints[2];
var endpoint4 = (RouteEndpoint)route.Endpoints[3];
var apiControllerName = ControllerExtensions.GetControllerName<Testing1Controller>();
Assert.AreEqual(
$"umbraco/backoffice/api/{apiControllerName.ToLowerInvariant()}/{{action}}/{{id?}}",

View File

@@ -34,6 +34,29 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht
StartServerOnDebug = "false"
EndProjectSection
EndProject
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.New.Client", "http://localhost:3961", "{17E2067E-9D0F-4891-B6BA-CD828886B968}"
ProjectSection(WebsiteProperties) = preProject
UseIISExpress = "true"
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2"
Debug.AspNetCompiler.VirtualPath = "/localhost_3961"
Debug.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.New.Client\"
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\"
Debug.AspNetCompiler.Updateable = "true"
Debug.AspNetCompiler.ForceOverwrite = "true"
Debug.AspNetCompiler.FixedNames = "false"
Debug.AspNetCompiler.Debug = "True"
Release.AspNetCompiler.VirtualPath = "/localhost_3961"
Release.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.New.Client\"
Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\"
Release.AspNetCompiler.Updateable = "true"
Release.AspNetCompiler.ForceOverwrite = "true"
Release.AspNetCompiler.FixedNames = "false"
Release.AspNetCompiler.Debug = "False"
SlnRelativePath = "src\Umbraco.Web.UI.New.Client\"
DefaultWebSiteLanguage = "Visual C#"
StartServerOnDebug = "false"
EndProjectSection
EndProject
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "http://localhost:58896", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}"
ProjectSection(WebsiteProperties) = preProject
UseIISExpress = "true"
@@ -158,6 +181,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Imaging.ImageSh
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{05878304-40EB-4F84-B40B-91BDB70DE094}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI.New", "src\Umbraco.Web.UI.New\Umbraco.Web.UI.New.csproj", "{C55CA725-9F4E-4618-9435-6B8AE05DA14D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Api.Common", "src\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj", "{D48B5D6B-82FF-4235-986C-CDE646F41DEC}"
EndProject
Global
@@ -318,6 +342,12 @@ Global
{C280181E-597B-4AA5-82E7-D7017E928749}.Release|Any CPU.Build.0 = Release|Any CPU
{C280181E-597B-4AA5-82E7-D7017E928749}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{C280181E-597B-4AA5-82E7-D7017E928749}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.Release|Any CPU.Build.0 = Release|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU
{C55CA725-9F4E-4618-9435-6B8AE05DA14D}.SkipTests|Any CPU.Build.0 = Debug|Any CPU
{D48B5D6B-82FF-4235-986C-CDE646F41DEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D48B5D6B-82FF-4235-986C-CDE646F41DEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D48B5D6B-82FF-4235-986C-CDE646F41DEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -346,6 +376,8 @@ Global
{2B47AD9F-FFF1-448A-88F1-D4F568811738} = {F2BF84D9-0A14-40AF-A0F3-B9BBBBC16A44}
{25AECCB5-B187-4406-844B-91B8FF0FCB37} = {2B47AD9F-FFF1-448A-88F1-D4F568811738}
{EA628ABD-624E-4AF3-B548-6710D4D66531} = {2B47AD9F-FFF1-448A-88F1-D4F568811738}
{17E2067E-9D0F-4891-B6BA-CD828886B968} = {995D9EFA-8BB1-4333-80AD-C525A06FD984}
{C55CA725-9F4E-4618-9435-6B8AE05DA14D} = {995D9EFA-8BB1-4333-80AD-C525A06FD984}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}