Skip to content

[release/6.0] Initial Bootstrap v4 SxS support #36378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins)
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<header>
<h1>@ViewData["Title"]</h1>
@{
if (User.Identity.IsAuthenticated)
if (User.Identity!.IsAuthenticated)
{
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@{
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
{
Layout = (string)parentLayout;
Layout = (string)parentLayout!;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
@if ((bool)ViewData["ManageNav.HasExternalLogins"])
@if ((bool)(ViewData["ManageNav.HasExternalLogins"] ?? false))
{
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins)
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins)
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<header>
<h1>@ViewData["Title"]</h1>
@{
if (User.Identity.IsAuthenticated)
if (User.Identity!.IsAuthenticated)
{
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@{
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
{
Layout = (string)parentLayout;
Layout = (string)parentLayout!;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>
<li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">Password</a></li>
@if ((bool)ViewData["ManageNav.HasExternalLogins"])
@if ((bool)(ViewData["ManageNav.HasExternalLogins"] ?? false))
{
<li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div>
<p>
@foreach (var provider in Model.ExternalLogins)
@foreach (var provider in Model.ExternalLogins!)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
Expand Down
120 changes: 119 additions & 1 deletion src/Identity/UI/src/IdentityBuilderUIExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Linq;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Hosting;

namespace Microsoft.AspNetCore.Identity
{
Expand All @@ -27,7 +32,29 @@ public static class IdentityBuilderUIExtensions
public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
{
builder.AddSignInManager();
builder.Services.AddMvc();
builder.Services
.AddMvc()
.ConfigureApplicationPartManager(apm =>
{
// We try to resolve the UI framework that was used by looking at the entry assembly.
// When an app runs, the entry assembly will point to the built app. In some rare cases
// (functional testing) the app assembly will be different, and we'll try to locate it through
// the same mechanism that MVC uses today.
// Finally, if for some reason we aren't able to find the assembly, we'll use our default value
// (Bootstrap5)
if (!TryResolveUIFramework(Assembly.GetEntryAssembly(), out var framework) &&
!TryResolveUIFramework(GetApplicationAssembly(builder), out framework))
{
framework = default;
}

var parts = new ConsolidatedAssemblyApplicationPartFactory().GetApplicationParts(typeof(IdentityBuilderUIExtensions).Assembly);
foreach (var part in parts)
{
apm.ApplicationParts.Add(part);
}
apm.FeatureProviders.Add(new ViewVersionFeatureProvider(framework));
});

builder.Services.ConfigureOptions(
typeof(IdentityDefaultUIConfigureOptions<>)
Expand All @@ -36,5 +63,96 @@ public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)

return builder;
}

private static Assembly GetApplicationAssembly(IdentityBuilder builder)
{
// This is the same logic that MVC follows to find the application assembly.
var environment = builder.Services.Where(d => d.ServiceType == typeof(IWebHostEnvironment)).ToArray();
var applicationName = ((IWebHostEnvironment)environment.LastOrDefault()?.ImplementationInstance)
.ApplicationName;

if (applicationName == null)
{
return null;
}
var appAssembly = Assembly.Load(applicationName);
return appAssembly;
}

private static bool TryResolveUIFramework(Assembly assembly, out UIFramework uiFramework)
{
uiFramework = default;

var metadata = assembly?.GetCustomAttributes<UIFrameworkAttribute>()
.SingleOrDefault()?.UIFramework; // Bootstrap5 is the default
if (metadata == null)
{
return false;
}

// If we find the metadata there must be a valid framework here.
if (!Enum.TryParse(metadata, ignoreCase: true, out uiFramework))
{
var enumValues = string.Join(", ", Enum.GetNames(typeof(UIFramework)).Select(v => $"'{v}'"));
throw new InvalidOperationException(
$"Found an invalid value for the 'IdentityUIFrameworkVersion'. Valid values are {enumValues}");
}

return true;
}

internal class ViewVersionFeatureProvider : IApplicationFeatureProvider<ViewsFeature>
{
private readonly UIFramework _framework;

public ViewVersionFeatureProvider(UIFramework framework) => _framework = framework;

public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{
var viewsToRemove = new List<CompiledViewDescriptor>();
foreach (var descriptor in feature.ViewDescriptors)
{
if (IsIdentityUIView(descriptor))
{
switch (_framework)
{
case UIFramework.Bootstrap4:
if (descriptor.Type.FullName.Contains("V5"))
{
// Remove V5 views
viewsToRemove.Add(descriptor);
}
else
{
// Fix up paths to eliminate version subdir
descriptor.RelativePath = descriptor.RelativePath.Replace("V4/", "");
}
break;
case UIFramework.Bootstrap5:
if (descriptor.Type.FullName.Contains("V4"))
{
// Remove V4 views
viewsToRemove.Add(descriptor);
}
else
{
// Fix up paths to eliminate version subdir
descriptor.RelativePath = descriptor.RelativePath.Replace("V5/", "");
}
break;
default:
throw new InvalidOperationException($"Unknown framework: {_framework}");
}
}
}

foreach (var descriptorToRemove in viewsToRemove)
{
feature.ViewDescriptors.Remove(descriptorToRemove);
}
}

private static bool IsIdentityUIView(CompiledViewDescriptor desc) => desc.RelativePath.StartsWith("/Areas/Identity", StringComparison.OrdinalIgnoreCase) && desc.Type.Assembly == typeof(IdentityBuilderUIExtensions).Assembly;
}
}
}
93 changes: 88 additions & 5 deletions src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Description>ASP.NET Core Identity UI is the default Razor Pages built-in UI for the ASP.NET Core Identity framework.</Description>
Expand All @@ -8,14 +8,24 @@
<PackageTags>aspnetcore;identity;membership;razorpages</PackageTags>
<EnableDefaultRazorGenerateItems>false</EnableDefaultRazorGenerateItems>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<StaticWebAssetBasePath>Identity</StaticWebAssetBasePath>

<PackageThirdPartyNoticesFile>$(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.TXT</PackageThirdPartyNoticesFile>
<Nullable>disable</Nullable>

<StaticWebAssetBasePath>Identity</StaticWebAssetBasePath>
<ProvideApplicationPartFactoryAttributeTypeName>Microsoft.AspNetCore.Mvc.ApplicationParts.NullApplicationPartFactory, Microsoft.AspNetCore.Mvc.Core</ProvideApplicationPartFactoryAttributeTypeName>
<GenerateStaticWebAssetsPackTargetsDependsOn>_GenerateIdentityUIPackItems;$(GenerateStaticWebAssetsPackTargetsDependsOn)</GenerateStaticWebAssetsPackTargetsDependsOn>
<DisableStaticWebAssetsBuildPropsFileGeneration>true</DisableStaticWebAssetsBuildPropsFileGeneration>
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
<StaticWebAssetsDisableProjectBuildMultiTargetingPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildMultiTargetingPropsFileGeneration>
<StaticWebAssetsDisableProjectBuildTransitivePropsFileGeneration>true</StaticWebAssetsDisableProjectBuildTransitivePropsFileGeneration>
<StaticWebAssetsGetBuildAssetsTargets>GetIdentityUIAssets</StaticWebAssetsGetBuildAssetsTargets>
</PropertyGroup>

<ItemGroup>
<Content Remove="@(Content)" />
<Content Include="wwwroot\**\*" Pack="true" />
<None Include="build\*" Pack="true" PackagePath="build\" />
<None Include="buildMultiTargeting\*" Pack="true" PackagePath="buildMultiTargeting\" />
<None Include="buildTransitive\*" Pack="true" PackagePath="buildTransitive\" />
</ItemGroup>

<ItemGroup>
Expand All @@ -28,10 +38,83 @@

<Target Name="SetupRazorInputs" BeforeTargets="ResolveRazorGenerateInputs">
<ItemGroup>
<_RazorGenerate Include="Areas\Identity\Pages\V5\**\*.cshtml" />
<_RazorGenerate Include="Areas\Identity\Pages\**\*.cshtml" />

<RazorGenerate Include="@(_RazorGenerate)" Link="Areas\Identity\Pages\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Target>

<Target Name="GetIdentityUIAssets" Returns="@(ReferenceAsset)">
<PropertyGroup>
<_ReferenceAssetContentRoot Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap5'">assets/V5</_ReferenceAssetContentRoot>
<_ReferenceAssetContentRoot Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap4'">assets/V4</_ReferenceAssetContentRoot>
</PropertyGroup>
<ItemGroup>
<ReferenceAssetCandidates Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap5'" Include="assets\V5\**" />
<ReferenceAssetCandidates Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap4'" Include="assets\V4\**" />
<ReferenceAssetCandidates>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
<ContentRoot>$([System.IO.Path]::GetFullPath($(_ReferenceAssetContentRoot)))</ContentRoot>
</ReferenceAssetCandidates>
</ItemGroup>
<DefineStaticWebAssets
Condition="'@(ReferenceAssetCandidates->Count())' != '0'"
CandidateAssets="@(ReferenceAssetCandidates)"
SourceId="$(PackageId)"
SourceType="Project"
AssetKind="All"
BasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="Assets" ItemName="ReferenceAsset" />
</DefineStaticWebAssets>
<ItemGroup>
<ReferenceAsset>
<ResultType>StaticWebAsset</ResultType>
</ReferenceAsset>
</ItemGroup>
</Target>

<Target Name="_GenerateIdentityUIPackItems">
<ItemGroup>
<V4AssetsCandidates Include="assets\V4\**" />
<V4AssetsCandidates>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</V4AssetsCandidates>
<V5AssetsCandidates Include="assets\V5\**" />
<V5AssetsCandidates>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</V5AssetsCandidates>
</ItemGroup>
<DefineStaticWebAssets Condition="'@(V4AssetsCandidates-&gt;Count())' != '0'" CandidateAssets="@(V4AssetsCandidates)" SourceId="$(PackageId)" SourceType="Discovered" AssetKind="All" ContentRoot="assets/V4" BasePath="$(StaticWebAssetBasePath)">
<Output TaskParameter="Assets" ItemName="V4Assets" />
</DefineStaticWebAssets>
<DefineStaticWebAssets Condition="'@(V5AssetsCandidates-&gt;Count())' != '0'" CandidateAssets="@(V5AssetsCandidates)" SourceId="$(PackageId)" SourceType="Discovered" AssetKind="All" ContentRoot="assets/V5" BasePath="$(StaticWebAssetBasePath)">
<Output TaskParameter="Assets" ItemName="V5Assets" />
</DefineStaticWebAssets>

<GenerateStaticWebAsssetsPropsFile StaticWebAssets="@(V4Assets)" PackagePathPrefix="staticwebassets/V4" TargetPropsFilePath="$(IntermediateOutputPath)IdentityUI.V4.targets" />
<GenerateStaticWebAsssetsPropsFile StaticWebAssets="@(V5Assets)" PackagePathPrefix="staticwebassets/V5" TargetPropsFilePath="$(IntermediateOutputPath)IdentityUI.V5.targets" />

<ComputeStaticWebAssetsTargetPaths Assets="@(V4Assets)" PathPrefix="staticwebassets/V4" AdjustPathsForPack="true">
<Output TaskParameter="AssetsWithTargetPath" ItemName="_PackV4Asset" />
</ComputeStaticWebAssetsTargetPaths>
<ComputeStaticWebAssetsTargetPaths Assets="@(V5Assets)" PathPrefix="staticwebassets/V5" AdjustPathsForPack="true">
<Output TaskParameter="AssetsWithTargetPath" ItemName="_PackV5Asset" />
</ComputeStaticWebAssetsTargetPaths>

<ItemGroup>
<TfmSpecificPackageFile Include="$(IntermediateOutputPath)IdentityUI.V4.targets">
<PackagePath>build\Microsoft.AspNetCore.StaticWebAssets.V4.targets</PackagePath>
</TfmSpecificPackageFile>
<TfmSpecificPackageFile Include="%(_PackV4Asset.Identity)">
<PackagePath>%(_PackV4Asset.TargetPath)</PackagePath>
</TfmSpecificPackageFile>
<TfmSpecificPackageFile Include="$(IntermediateOutputPath)IdentityUI.V5.targets">
<PackagePath>build\Microsoft.AspNetCore.StaticWebAssets.V5.targets</PackagePath>
</TfmSpecificPackageFile>
<TfmSpecificPackageFile Include="%(_PackV5Asset.Identity)">
<PackagePath>%(_PackV5Asset.TargetPath)</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
</Target>
</Project>
Loading