/
IdentityBuilderUIExtensions.cs
158 lines (142 loc) · 7.12 KB
/
IdentityBuilderUIExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// 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
{
/// <summary>
/// Default UI extensions to <see cref="IdentityBuilder"/>.
/// </summary>
public static class IdentityBuilderUIExtensions
{
/// <summary>
/// Adds a default, self-contained UI for Identity to the application using
/// Razor Pages in an area named Identity.
/// </summary>
/// <remarks>
/// In order to use the default UI, the application must be using <see cref="Microsoft.AspNetCore.Mvc"/>,
/// <see cref="Microsoft.AspNetCore.StaticFiles"/> and contain a <c>_LoginPartial</c> partial view that
/// can be found by the application.
/// </remarks>
/// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
{
builder.AddSignInManager();
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<>)
.MakeGenericType(builder.UserType));
builder.Services.TryAddTransient<IEmailSender, EmailSender>();
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;
}
}
}