Skip to content

Commit e2eccf4

Browse files
github-actions[bot]javiercnHaoK
committed
[release/6.0] Initial Bootstrap v4 SxS support (#36378)
* Add props and build files * Bring back V4 files tmp Setup V5 stuff * Add back v5 files too Restore V4 files Undo diffs Update Testing.DefaultWebSite.StaticWebAssets.V5.xml Fix tests Checkpoint Fix Undo Try bootstrap4 codepath Stuff Checkpoint before rebase * Correct target names * Fix views * Cleanup * Fix test * Bring back targets * Make things work * PR feedback * Fix for content type * Harden the view version logic * Fix test * Nullable warning fixes * Fix nullable warning another way * Fix V4 nullability warnings * Another nullability fix * Fix CI Co-authored-by: Javier Calvarro Nelson <[email protected]> Co-authored-by: Hao Kung <[email protected]>
1 parent 2d98236 commit e2eccf4

File tree

119 files changed

+40134
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+40134
-42
lines changed

src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Login.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
6868
<div>
6969
<p>
70-
@foreach (var provider in Model.ExternalLogins)
70+
@foreach (var provider in Model.ExternalLogins!)
7171
{
7272
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
7373
}

src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Logout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<header>
88
<h1>@ViewData["Title"]</h1>
99
@{
10-
if (User.Identity.IsAuthenticated)
10+
if (User.Identity!.IsAuthenticated)
1111
{
1212
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
1313
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>

src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Manage/_Layout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@{
22
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
33
{
4-
Layout = (string)parentLayout;
4+
Layout = (string)parentLayout!;
55
}
66
else
77
{

src/Identity/UI/src/Areas/Identity/Pages/V4/Account/Register.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
5050
<div>
5151
<p>
52-
@foreach (var provider in Model.ExternalLogins)
52+
@foreach (var provider in Model.ExternalLogins!)
5353
{
5454
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
5555
}

src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Login.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
6868
<div>
6969
<p>
70-
@foreach (var provider in Model.ExternalLogins)
70+
@foreach (var provider in Model.ExternalLogins!)
7171
{
7272
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
7373
}

src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Logout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<header>
88
<h1>@ViewData["Title"]</h1>
99
@{
10-
if (User.Identity.IsAuthenticated)
10+
if (User.Identity!.IsAuthenticated)
1111
{
1212
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/", new { area = "" })" method="post">
1313
<button type="submit" class="nav-link btn btn-link text-dark">Click here to Logout</button>

src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Manage/_Layout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@{
22
if (ViewData.TryGetValue("ParentLayout", out var parentLayout))
33
{
4-
Layout = (string)parentLayout;
4+
Layout = (string)parentLayout!;
55
}
66
else
77
{

src/Identity/UI/src/Areas/Identity/Pages/V5/Account/Register.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
5050
<div>
5151
<p>
52-
@foreach (var provider in Model.ExternalLogins)
52+
@foreach (var provider in Model.ExternalLogins!)
5353
{
5454
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
5555
}

src/Identity/UI/src/IdentityBuilderUIExtensions.cs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Reflection;
5+
using System.Linq;
46
using Microsoft.AspNetCore.Identity.UI;
57
using Microsoft.AspNetCore.Identity.UI.Services;
8+
using Microsoft.AspNetCore.Mvc.ApplicationParts;
69
using Microsoft.Extensions.DependencyInjection;
710
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
12+
using Microsoft.AspNetCore.Hosting;
813

914
namespace Microsoft.AspNetCore.Identity;
1015

@@ -27,7 +32,29 @@ public static class IdentityBuilderUIExtensions
2732
public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
2833
{
2934
builder.AddSignInManager();
30-
builder.Services.AddMvc();
35+
builder.Services
36+
.AddMvc()
37+
.ConfigureApplicationPartManager(apm =>
38+
{
39+
// We try to resolve the UI framework that was used by looking at the entry assembly.
40+
// When an app runs, the entry assembly will point to the built app. In some rare cases
41+
// (functional testing) the app assembly will be different, and we'll try to locate it through
42+
// the same mechanism that MVC uses today.
43+
// Finally, if for some reason we aren't able to find the assembly, we'll use our default value
44+
// (Bootstrap5)
45+
if (!TryResolveUIFramework(Assembly.GetEntryAssembly(), out var framework) &&
46+
!TryResolveUIFramework(GetApplicationAssembly(builder), out framework))
47+
{
48+
framework = default;
49+
}
50+
51+
var parts = new ConsolidatedAssemblyApplicationPartFactory().GetApplicationParts(typeof(IdentityBuilderUIExtensions).Assembly);
52+
foreach (var part in parts)
53+
{
54+
apm.ApplicationParts.Add(part);
55+
}
56+
apm.FeatureProviders.Add(new ViewVersionFeatureProvider(framework));
57+
});
3158

3259
builder.Services.ConfigureOptions(
3360
typeof(IdentityDefaultUIConfigureOptions<>)
@@ -36,4 +63,95 @@ public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
3663

3764
return builder;
3865
}
66+
67+
private static Assembly GetApplicationAssembly(IdentityBuilder builder)
68+
{
69+
// This is the same logic that MVC follows to find the application assembly.
70+
var environment = builder.Services.Where(d => d.ServiceType == typeof(IWebHostEnvironment)).ToArray();
71+
var applicationName = ((IWebHostEnvironment)environment.LastOrDefault()?.ImplementationInstance)
72+
.ApplicationName;
73+
74+
if (applicationName == null)
75+
{
76+
return null;
77+
}
78+
var appAssembly = Assembly.Load(applicationName);
79+
return appAssembly;
80+
}
81+
82+
private static bool TryResolveUIFramework(Assembly assembly, out UIFramework uiFramework)
83+
{
84+
uiFramework = default;
85+
86+
var metadata = assembly?.GetCustomAttributes<UIFrameworkAttribute>()
87+
.SingleOrDefault()?.UIFramework; // Bootstrap5 is the default
88+
if (metadata == null)
89+
{
90+
return false;
91+
}
92+
93+
// If we find the metadata there must be a valid framework here.
94+
if (!Enum.TryParse(metadata, ignoreCase: true, out uiFramework))
95+
{
96+
var enumValues = string.Join(", ", Enum.GetNames(typeof(UIFramework)).Select(v => $"'{v}'"));
97+
throw new InvalidOperationException(
98+
$"Found an invalid value for the 'IdentityUIFrameworkVersion'. Valid values are {enumValues}");
99+
}
100+
101+
return true;
102+
}
103+
104+
internal class ViewVersionFeatureProvider : IApplicationFeatureProvider<ViewsFeature>
105+
{
106+
private readonly UIFramework _framework;
107+
108+
public ViewVersionFeatureProvider(UIFramework framework) => _framework = framework;
109+
110+
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
111+
{
112+
var viewsToRemove = new List<CompiledViewDescriptor>();
113+
foreach (var descriptor in feature.ViewDescriptors)
114+
{
115+
if (IsIdentityUIView(descriptor))
116+
{
117+
switch (_framework)
118+
{
119+
case UIFramework.Bootstrap4:
120+
if (descriptor.Type.FullName.Contains("V5"))
121+
{
122+
// Remove V5 views
123+
viewsToRemove.Add(descriptor);
124+
}
125+
else
126+
{
127+
// Fix up paths to eliminate version subdir
128+
descriptor.RelativePath = descriptor.RelativePath.Replace("V4/", "");
129+
}
130+
break;
131+
case UIFramework.Bootstrap5:
132+
if (descriptor.Type.FullName.Contains("V4"))
133+
{
134+
// Remove V4 views
135+
viewsToRemove.Add(descriptor);
136+
}
137+
else
138+
{
139+
// Fix up paths to eliminate version subdir
140+
descriptor.RelativePath = descriptor.RelativePath.Replace("V5/", "");
141+
}
142+
break;
143+
default:
144+
throw new InvalidOperationException($"Unknown framework: {_framework}");
145+
}
146+
}
147+
}
148+
149+
foreach (var descriptorToRemove in viewsToRemove)
150+
{
151+
feature.ViewDescriptors.Remove(descriptorToRemove);
152+
}
153+
}
154+
155+
private static bool IsIdentityUIView(CompiledViewDescriptor desc) => desc.RelativePath.StartsWith("/Areas/Identity", StringComparison.OrdinalIgnoreCase) && desc.Type.Assembly == typeof(IdentityBuilderUIExtensions).Assembly;
156+
}
39157
}

src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Razor">
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core Identity UI is the default Razor Pages built-in UI for the ASP.NET Core Identity framework.</Description>
@@ -8,14 +8,24 @@
88
<PackageTags>aspnetcore;identity;membership;razorpages</PackageTags>
99
<EnableDefaultRazorGenerateItems>false</EnableDefaultRazorGenerateItems>
1010
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
11-
<StaticWebAssetBasePath>Identity</StaticWebAssetBasePath>
11+
1212
<PackageThirdPartyNoticesFile>$(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.TXT</PackageThirdPartyNoticesFile>
1313
<Nullable>disable</Nullable>
14+
15+
<StaticWebAssetBasePath>Identity</StaticWebAssetBasePath>
16+
<ProvideApplicationPartFactoryAttributeTypeName>Microsoft.AspNetCore.Mvc.ApplicationParts.NullApplicationPartFactory, Microsoft.AspNetCore.Mvc.Core</ProvideApplicationPartFactoryAttributeTypeName>
17+
<GenerateStaticWebAssetsPackTargetsDependsOn>_GenerateIdentityUIPackItems;$(GenerateStaticWebAssetsPackTargetsDependsOn)</GenerateStaticWebAssetsPackTargetsDependsOn>
18+
<DisableStaticWebAssetsBuildPropsFileGeneration>true</DisableStaticWebAssetsBuildPropsFileGeneration>
19+
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
20+
<StaticWebAssetsDisableProjectBuildMultiTargetingPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildMultiTargetingPropsFileGeneration>
21+
<StaticWebAssetsDisableProjectBuildTransitivePropsFileGeneration>true</StaticWebAssetsDisableProjectBuildTransitivePropsFileGeneration>
22+
<StaticWebAssetsGetBuildAssetsTargets>GetIdentityUIAssets</StaticWebAssetsGetBuildAssetsTargets>
1423
</PropertyGroup>
1524

1625
<ItemGroup>
17-
<Content Remove="@(Content)" />
18-
<Content Include="wwwroot\**\*" Pack="true" />
26+
<None Include="build\*" Pack="true" PackagePath="build\" />
27+
<None Include="buildMultiTargeting\*" Pack="true" PackagePath="buildMultiTargeting\" />
28+
<None Include="buildTransitive\*" Pack="true" PackagePath="buildTransitive\" />
1929
</ItemGroup>
2030

2131
<ItemGroup>
@@ -28,10 +38,83 @@
2838

2939
<Target Name="SetupRazorInputs" BeforeTargets="ResolveRazorGenerateInputs">
3040
<ItemGroup>
31-
<_RazorGenerate Include="Areas\Identity\Pages\V5\**\*.cshtml" />
41+
<_RazorGenerate Include="Areas\Identity\Pages\**\*.cshtml" />
3242

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

47+
<Target Name="GetIdentityUIAssets" Returns="@(ReferenceAsset)">
48+
<PropertyGroup>
49+
<_ReferenceAssetContentRoot Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap5'">assets/V5</_ReferenceAssetContentRoot>
50+
<_ReferenceAssetContentRoot Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap4'">assets/V4</_ReferenceAssetContentRoot>
51+
</PropertyGroup>
52+
<ItemGroup>
53+
<ReferenceAssetCandidates Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap5'" Include="assets\V5\**" />
54+
<ReferenceAssetCandidates Condition="'$(IdentityDefaultUIFramework)' == 'Bootstrap4'" Include="assets\V4\**" />
55+
<ReferenceAssetCandidates>
56+
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
57+
<ContentRoot>$([System.IO.Path]::GetFullPath($(_ReferenceAssetContentRoot)))</ContentRoot>
58+
</ReferenceAssetCandidates>
59+
</ItemGroup>
60+
<DefineStaticWebAssets
61+
Condition="'@(ReferenceAssetCandidates->Count())' != '0'"
62+
CandidateAssets="@(ReferenceAssetCandidates)"
63+
SourceId="$(PackageId)"
64+
SourceType="Project"
65+
AssetKind="All"
66+
BasePath="$(StaticWebAssetBasePath)"
67+
>
68+
<Output TaskParameter="Assets" ItemName="ReferenceAsset" />
69+
</DefineStaticWebAssets>
70+
<ItemGroup>
71+
<ReferenceAsset>
72+
<ResultType>StaticWebAsset</ResultType>
73+
</ReferenceAsset>
74+
</ItemGroup>
75+
</Target>
76+
77+
<Target Name="_GenerateIdentityUIPackItems">
78+
<ItemGroup>
79+
<V4AssetsCandidates Include="assets\V4\**" />
80+
<V4AssetsCandidates>
81+
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
82+
</V4AssetsCandidates>
83+
<V5AssetsCandidates Include="assets\V5\**" />
84+
<V5AssetsCandidates>
85+
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
86+
</V5AssetsCandidates>
87+
</ItemGroup>
88+
<DefineStaticWebAssets Condition="'@(V4AssetsCandidates-&gt;Count())' != '0'" CandidateAssets="@(V4AssetsCandidates)" SourceId="$(PackageId)" SourceType="Discovered" AssetKind="All" ContentRoot="assets/V4" BasePath="$(StaticWebAssetBasePath)">
89+
<Output TaskParameter="Assets" ItemName="V4Assets" />
90+
</DefineStaticWebAssets>
91+
<DefineStaticWebAssets Condition="'@(V5AssetsCandidates-&gt;Count())' != '0'" CandidateAssets="@(V5AssetsCandidates)" SourceId="$(PackageId)" SourceType="Discovered" AssetKind="All" ContentRoot="assets/V5" BasePath="$(StaticWebAssetBasePath)">
92+
<Output TaskParameter="Assets" ItemName="V5Assets" />
93+
</DefineStaticWebAssets>
94+
95+
<GenerateStaticWebAsssetsPropsFile StaticWebAssets="@(V4Assets)" PackagePathPrefix="staticwebassets/V4" TargetPropsFilePath="$(IntermediateOutputPath)IdentityUI.V4.targets" />
96+
<GenerateStaticWebAsssetsPropsFile StaticWebAssets="@(V5Assets)" PackagePathPrefix="staticwebassets/V5" TargetPropsFilePath="$(IntermediateOutputPath)IdentityUI.V5.targets" />
97+
98+
<ComputeStaticWebAssetsTargetPaths Assets="@(V4Assets)" PathPrefix="staticwebassets/V4" AdjustPathsForPack="true">
99+
<Output TaskParameter="AssetsWithTargetPath" ItemName="_PackV4Asset" />
100+
</ComputeStaticWebAssetsTargetPaths>
101+
<ComputeStaticWebAssetsTargetPaths Assets="@(V5Assets)" PathPrefix="staticwebassets/V5" AdjustPathsForPack="true">
102+
<Output TaskParameter="AssetsWithTargetPath" ItemName="_PackV5Asset" />
103+
</ComputeStaticWebAssetsTargetPaths>
104+
105+
<ItemGroup>
106+
<TfmSpecificPackageFile Include="$(IntermediateOutputPath)IdentityUI.V4.targets">
107+
<PackagePath>build\Microsoft.AspNetCore.StaticWebAssets.V4.targets</PackagePath>
108+
</TfmSpecificPackageFile>
109+
<TfmSpecificPackageFile Include="%(_PackV4Asset.Identity)">
110+
<PackagePath>%(_PackV4Asset.TargetPath)</PackagePath>
111+
</TfmSpecificPackageFile>
112+
<TfmSpecificPackageFile Include="$(IntermediateOutputPath)IdentityUI.V5.targets">
113+
<PackagePath>build\Microsoft.AspNetCore.StaticWebAssets.V5.targets</PackagePath>
114+
</TfmSpecificPackageFile>
115+
<TfmSpecificPackageFile Include="%(_PackV5Asset.Identity)">
116+
<PackagePath>%(_PackV5Asset.TargetPath)</PackagePath>
117+
</TfmSpecificPackageFile>
118+
</ItemGroup>
119+
</Target>
37120
</Project>

0 commit comments

Comments
 (0)