[Identity] Move to use static web assets support. (#11029)

* Moves Identity UI to use Static Web Assets
  * Removes the static files as embedded content.
  * Stops plugging the static assets through the embedded file provider.
  * Selects the UI framework at build time instead of runtime.
This commit is contained in:
Javier Calvarro Nelson 2019-07-25 17:34:45 +02:00 committed by GitHub
parent 7a496ec162
commit 143c101693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 723 additions and 197 deletions

View File

@ -0,0 +1,24 @@
{
"solution": {
"path": "Identity.sln",
"projects": [
"ApiAuthorization.IdentityServer\\samples\\ApiAuthSample\\ApiAuthSample.csproj",
"ApiAuthorization.IdentityServer\\src\\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj",
"ApiAuthorization.IdentityServer\\test\\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Tests.csproj",
"Core\\src\\Microsoft.AspNetCore.Identity.csproj",
"EntityFrameworkCore\\src\\Microsoft.AspNetCore.Identity.EntityFrameworkCore.csproj",
"EntityFrameworkCore\\test\\EF.InMemory.Test\\Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test.csproj",
"EntityFrameworkCore\\test\\EF.Test\\Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test.csproj",
"Extensions.Core\\src\\Microsoft.Extensions.Identity.Core.csproj",
"Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj",
"Specification.Tests\\src\\Microsoft.AspNetCore.Identity.Specification.Tests.csproj",
"UI\\src\\Microsoft.AspNetCore.Identity.UI.csproj",
"samples\\IdentitySample.DefaultUI\\IdentitySample.DefaultUI.csproj",
"samples\\IdentitySample.Mvc\\IdentitySample.Mvc.csproj",
"test\\Identity.FunctionalTests\\Microsoft.AspNetCore.Identity.FunctionalTests.csproj",
"test\\Identity.Test\\Microsoft.AspNetCore.Identity.Test.csproj",
"test\\InMemory.Test\\Microsoft.AspNetCore.Identity.InMemory.Test.csproj",
"testassets\\Identity.DefaultUI.WebSite\\Identity.DefaultUI.WebSite.csproj"
]
}
}

View File

@ -6,20 +6,15 @@ namespace Microsoft.AspNetCore.Identity
public static partial class IdentityBuilderUIExtensions
{
public static Microsoft.AspNetCore.Identity.IdentityBuilder AddDefaultUI(this Microsoft.AspNetCore.Identity.IdentityBuilder builder) { throw null; }
public static Microsoft.AspNetCore.Identity.IdentityBuilder AddDefaultUI(this Microsoft.AspNetCore.Identity.IdentityBuilder builder, Microsoft.AspNetCore.Identity.UI.UIFramework framework) { throw null; }
}
}
namespace Microsoft.AspNetCore.Identity.UI
{
public partial class DefaultUIOptions
[System.AttributeUsageAttribute(System.AttributeTargets.Assembly, Inherited=false, AllowMultiple=false)]
public sealed partial class UIFrameworkAttribute : System.Attribute
{
public DefaultUIOptions() { }
public Microsoft.AspNetCore.Identity.UI.UIFramework UIFramework { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public enum UIFramework
{
Bootstrap3 = 0,
Bootstrap4 = 1,
public UIFrameworkAttribute(string uiFramework) { }
public string UIFramework { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
}
namespace Microsoft.AspNetCore.Identity.UI.Services

View File

@ -1,16 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Identity.UI
{
/// <summary>
/// Options for the default Identity UI
/// </summary>
public class DefaultUIOptions
{
/// <summary>
/// Gets or sets the <see cref="UIFramework"/> to use with the default UI.
/// </summary>
public UIFramework UIFramework { get; set; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
@ -30,35 +31,16 @@ namespace Microsoft.AspNetCore.Identity
/// </remarks>
/// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder) => builder.AddDefaultUI(UIFramework.Bootstrap4);
/// <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>
/// <param name="framework">The <see cref="UIFramework"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddDefaultUI(
this IdentityBuilder builder,
UIFramework framework)
public static IdentityBuilder AddDefaultUI(this IdentityBuilder builder)
{
builder.AddSignInManager();
AddRelatedParts(builder, framework);
AddRelatedParts(builder);
builder.Services.ConfigureOptions(
typeof(IdentityDefaultUIConfigureOptions<>)
.MakeGenericType(builder.UserType));
builder.Services.TryAddTransient<IEmailSender, EmailSender>();
builder.Services.Configure<DefaultUIOptions>(o => o.UIFramework = framework);
return builder;
}
@ -69,8 +51,20 @@ namespace Microsoft.AspNetCore.Identity
[UIFramework.Bootstrap4] = "Microsoft.AspNetCore.Identity.UI.Views.V4",
};
private static void AddRelatedParts(IdentityBuilder builder, UIFramework framework)
private static void AddRelatedParts(IdentityBuilder builder)
{
// 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
// (Bootstrap4)
if (!TryResolveUIFramework(Assembly.GetEntryAssembly(), out var framework) ||
!TryResolveUIFramework(GetApplicationAssembly(builder), out framework))
{
framework = default;
}
var mvcBuilder = builder.Services
.AddMvc()
.ConfigureApplicationPartManager(partManager =>
@ -131,5 +125,38 @@ namespace Microsoft.AspNetCore.Identity
}
});
}
private static Assembly GetApplicationAssembly(IdentityBuilder builder)
{
// Whis 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;
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; // Bootstrap4 is the default
if (metadata == null)
{
return false;
}
// If we find the metadata there must be a valid framework here.
if (!Enum.TryParse<UIFramework>(metadata, ignoreCase: true, out var 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;
}
}
}

View File

@ -3,34 +3,26 @@
using System;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.UI.Areas.Identity.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Identity.UI
{
internal class IdentityDefaultUIConfigureOptions<TUser> :
IPostConfigureOptions<RazorPagesOptions>,
IPostConfigureOptions<StaticFileOptions>,
IConfigureNamedOptions<CookieAuthenticationOptions> where TUser : class
{
private const string IdentityUIDefaultAreaName = "Identity";
public IdentityDefaultUIConfigureOptions(
IWebHostEnvironment environment,
IOptions<DefaultUIOptions> uiOptions)
{
IWebHostEnvironment environment) {
Environment = environment;
UiOptions = uiOptions;
}
public IWebHostEnvironment Environment { get; }
public IOptions<DefaultUIOptions> UiOptions { get; }
public void PostConfigure(string name, RazorPagesOptions options)
{
@ -50,30 +42,10 @@ namespace Microsoft.AspNetCore.Identity.UI
pam => pam.Filters.Add(new ExternalLoginsPageFilter<TUser>()));
}
public void PostConfigure(string name, StaticFileOptions options)
{
name = name ?? throw new ArgumentNullException(nameof(name));
options = options ?? throw new ArgumentNullException(nameof(options));
// Basic initialization in case the options weren't initialized by any other component
options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
if (options.FileProvider == null && Environment.WebRootFileProvider == null)
{
throw new InvalidOperationException("Missing FileProvider.");
}
options.FileProvider = options.FileProvider ?? Environment.WebRootFileProvider;
var basePath = UiOptions.Value.UIFramework == UIFramework.Bootstrap3 ? "wwwroot/V3" :
"wwwroot/V4";
// Add our provider
var filesProvider = new ManifestEmbeddedFileProvider(GetType().Assembly, basePath);
options.FileProvider = new CompositeFileProvider(options.FileProvider, filesProvider);
public void Configure(CookieAuthenticationOptions options) {
// Nothing to do here as Configure(string name, CookieAuthenticationOptions options) is hte one setting things up.
}
public void Configure(CookieAuthenticationOptions options) { }
public void Configure(string name, CookieAuthenticationOptions options)
{
name = name ?? throw new ArgumentNullException(nameof(name));

View File

@ -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>
@ -7,23 +7,30 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;identity;membership;razorpages</PackageTags>
<IsShippingPackage>true</IsShippingPackage>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<ProvideApplicationPartFactoryAttributeTypeName>Microsoft.AspNetCore.Mvc.ApplicationParts.NullApplicationPartFactory, Microsoft.AspNetCore.Mvc.Core</ProvideApplicationPartFactoryAttributeTypeName>
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish>
<EnableDefaultRazorGenerateItems>false</EnableDefaultRazorGenerateItems>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<DisableStaticWebAssetsBuildPropsFileGeneration>true</DisableStaticWebAssetsBuildPropsFileGeneration>
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
<GetCurrentProjectStaticWebAssetsDependsOn>
$(GetCurrentProjectStaticWebAssetsDependsOn);
_UpdatedIdentityUIStaticWebAssets
</GetCurrentProjectStaticWebAssetsDependsOn>
<IdentityUIFrameworkVersion Condition="'$(IdentityUIFrameworkVersion)' == ''">Bootstrap4</IdentityUIFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="wwwroot/**/*" />
<EmbeddedResource Remove="wwwroot/**/LICENSE*" />
<None Remove="wwwroot/**/LICENSE*" />
<Content Remove="@(Content)" />
<Content Include="wwwroot\**\*" Pack="true" />
<None Include="build\*" Pack="true" PackagePath="build\" />
<None Include="THIRD-PARTY-NOTICES.txt" Pack="true" PackagePath="/THIRD-PARTY-NOTICES.txt" />
<Content Update="wwwroot/**/*" Pack="false" />
<Content Update="**\*.cshtml" Pack="false" />
</ItemGroup>
<ItemGroup>
@ -40,6 +47,10 @@
<UIFrameworkVersionMoniker Include="V4" />
</ItemGroup>
<ItemGroup>
<Folder Include="build\" />
</ItemGroup>
<!-- Source build doesn't build this package -->
<Target Name="BuildRazorViews" DependsOnTargets="Compile" BeforeTargets="Build" Condition="'$(DotNetBuildFromSource)' != 'true'">
<Message Text="Building razor views assemblies" Importance="High" />
@ -63,6 +74,7 @@
<Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
<Output TaskParameter="DestinationFiles" ItemName="_RazorAssembly"/>
</Copy>
<Copy
@ -75,12 +87,10 @@
UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)">
<Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
<Output TaskParameter="DestinationFiles" ItemName="FileWrites" />
</Copy>
<Message
Importance="High"
Text="$(MSBuildProjectName) -&gt; %(_RazorAssembly.FullPath)" />
<Message Importance="High" Text="$(MSBuildProjectName) -&gt; %(_RazorAssembly.FullPath)" />
</Target>
@ -90,19 +100,15 @@
<Target Name="SetupRazorInputs">
<ItemGroup>
<_RazorGenerate
Include="Areas\Identity\Pages\$(UIFrameworkVersion)\**\*.cshtml" />
<_RazorGenerate Include="Areas\Identity\Pages\$(UIFrameworkVersion)\**\*.cshtml" />
<RazorGenerate
Include="@(_RazorGenerate)"
Link="Areas\Identity\Pages\%(RecursiveDir)%(Filename)%(Extension)" />
<RazorGenerate Include="@(_RazorGenerate)" Link="Areas\Identity\Pages\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Target>
<Target Name="BuildForUI" DependsOnTargets="SetupRazorInputs;RazorCompile" />
<Target
Name="_GetRazorDlls" BeforeTargets="GetCopyToOutputDirectoryItems">
<Target Name="_GetRazorDlls" BeforeTargets="GetCopyToOutputDirectoryItems">
<ItemGroup>
<_GeneratedRazorViews Include="$(TargetDir)$(TargetName).Views.%(UIFrameworkVersionMoniker.Identity).dll" />
@ -117,15 +123,13 @@
<Target Name="_AddRazorDlls" BeforeTargets="BuiltProjectOutputGroup">
<ItemGroup>
<BuiltProjectOutputGroupOutput
Include="$(IntermediateOutputPath)%(UIFrameworkVersionMoniker.Identity)\$(TargetName).Views.%(UIFrameworkVersionMoniker.Identity).dll" />
<BuiltProjectOutputGroupOutput Include="$(IntermediateOutputPath)%(UIFrameworkVersionMoniker.Identity)\$(TargetName).Views.%(UIFrameworkVersionMoniker.Identity).dll" />
</ItemGroup>
</Target>
<Target Name="_AddRazorPdbs" BeforeTargets="DebugSymbolsProjectOutputGroup">
<ItemGroup>
<DebugSymbolsProjectOutputGroupOutput
Include="$(IntermediateOutputPath)%(UIFrameworkVersionMoniker.Identity)\$(TargetName).Views.%(UIFrameworkVersionMoniker.Identity).pdb" />
<DebugSymbolsProjectOutputGroupOutput Include="$(IntermediateOutputPath)%(UIFrameworkVersionMoniker.Identity)\$(TargetName).Views.%(UIFrameworkVersionMoniker.Identity).pdb" />
</ItemGroup>
</Target>
@ -136,8 +140,34 @@
<ExpectedOutputFile Include="$(TargetDir)Microsoft.AspNetCore.Identity.UI.Views.V4.dll" />
</ItemGroup>
<Error Text="Unable to find precompiled view file %(ExpectedOutputFile.Identity)"
Condition="!Exists('%(ExpectedOutputFile.Identity)')" />
<Error Text="Unable to find precompiled view file %(ExpectedOutputFile.Identity)" Condition="!Exists('%(ExpectedOutputFile.Identity)')" />
</Target>
<Target Name="_UpdatedIdentityUIStaticWebAssets">
<ItemGroup>
<StaticWebAsset Remove="@(StaticWebAsset)" />
<_V3Content Include="wwwroot\V3\**" />
<_V4Content Include="wwwroot\V4\**" />
<StaticWebAsset Include="@(_V3Content->'%(FullPath)')" Condition="'$(IdentityUIFrameworkVersion)' == 'Bootstrap3'">
<SourceType></SourceType>
<SourceId>Microsoft.AspNetCore.Identity.UI</SourceId>
<ContentRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)wwwroot/V3'))</ContentRoot>
<BasePath>/Identity</BasePath>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</StaticWebAsset>
<StaticWebAsset Include="@(_V4Content->'%(FullPath)')" Condition="'$(IdentityUIFrameworkVersion)' == 'Bootstrap4'">
<SourceType></SourceType>
<SourceId>Microsoft.AspNetCore.Identity.UI</SourceId>
<ContentRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)wwwroot/V4'))</ContentRoot>
<BasePath>/Identity</BasePath>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</StaticWebAsset>
</ItemGroup>
</Target>
</Project>

View File

@ -1,21 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Identity.UI
{
/// <summary>
/// The list of supported presentation frameworks for the default UI
/// </summary>
public enum UIFramework
internal enum UIFramework
{
/// <summary>
/// Bootstrap 3
/// </summary>
Bootstrap3 = 0,
/// <summary>
/// Bootstrap 4
/// </summary>
Bootstrap4 = 1
// The default framework for a given release must be 0.
// So this needs to be updated in the future if we include more frameworks.
Bootstrap4 = 0,
Bootstrap3 = 1,
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Identity.UI
{
/// <summary>
/// The UIFramework Identity UI will use on the application.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
public sealed class UIFrameworkAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of <see cref="UIFrameworkAttribute"/>.
/// </summary>
/// <param name="uiFramework"></param>
public UIFrameworkAttribute(string uiFramework)
{
UIFramework = uiFramework;
}
/// <summary>
/// The UI Framework Identity UI will use.
/// </summary>
public string UIFramework { get; }
}
}

View File

@ -0,0 +1,31 @@
<Project>
<PropertyGroup>
<IdentityUIFrameworkVersion Condition="'$(IdentityUIFrameworkVersion)' == ''">Bootstrap4</IdentityUIFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="Microsoft.AspNetCore.Identity.UI.UIFrameworkAttribute">
<_Parameter1>$(IdentityUIFrameworkVersion)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup Condition="'$(IdentityUIFrameworkVersion)' == 'Bootstrap3'">
<StaticWebAsset Include="$(MSBuildThisFileDirectory)..\staticwebassets\V3\**">
<SourceType>Package</SourceType>
<SourceId>Microsoft.AspNetCore.Identity.UI</SourceId>
<ContentRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\staticwebassets\V3'))</ContentRoot>
<BasePath>/Identity</BasePath>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</StaticWebAsset>
</ItemGroup>
<ItemGroup Condition="'$(IdentityUIFrameworkVersion)' == 'Bootstrap4'">
<StaticWebAsset Include="$(MSBuildThisFileDirectory)..\staticwebassets\V4\**">
<SourceType>Package</SourceType>
<SourceId>Microsoft.AspNetCore.Identity.UI</SourceId>
<ContentRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\staticwebassets\V4'))</ContentRoot>
<BasePath>/Identity</BasePath>
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
</StaticWebAsset>
</ItemGroup>
</Project>

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,3 +0,0 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}

View File

@ -1,12 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Description>Identity sample MVC application on ASP.NET Core using the default UI</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UserSecretsId>aspnetcore-2ff9bc27-5e8c-4484-90ca-e3aace89b72a</UserSecretsId>
<IdentityUIFrameworkVersion>Bootstrap4</IdentityUIFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication.Facebook" />
<Reference Include="Microsoft.AspNetCore.Authentication.Google" />
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
@ -26,6 +28,7 @@
<Reference Include="Microsoft.EntityFrameworkCore.Tools" PrivateAssets="All" />
<Reference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.Extensions.Logging.Debug" />
</ItemGroup>
@ -34,4 +37,23 @@
<Folder Include="Properties\" />
</ItemGroup>
<!-- Include an assembly attribute into the built application assembly to indicate which flavor of the UI framework to use -->
<ItemGroup>
<AssemblyAttribute Include="Microsoft.AspNetCore.Identity.UI.UIFrameworkAttribute">
<_Parameter1>$(IdentityUIFrameworkVersion)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<PropertyGroup>
<ResolveStaticWebAssetsInputsDependsOn>_SetBootstrapFrameworkVersion</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<Target Name="_SetBootstrapFrameworkVersion">
<ItemGroup>
<_StaticWebAssetsProjectReference Condition="'%(FileName)' == 'Microsoft.AspNetCore.identity.UI'">
<AdditionalProperties>IdentityUIFrameworkVersion=$(IdentityUIFrameworkVersion)</AdditionalProperties>
</_StaticWebAssetsProjectReference>
</ItemGroup>
</Target>
</Project>

View File

@ -1,5 +1,6 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace IdentitySample.DefaultUI
{
@ -7,17 +8,19 @@ namespace IdentitySample.DefaultUI
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args)
.Build();
host.Run();
CreateHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>();
public static bool UseStartup { get; set; } = true;
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
if (UseStartup)
{
webBuilder.UseStartup<Startup>();
}
});
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Identity.DefaultUI.WebSite;
@ -6,10 +6,11 @@ using Identity.DefaultUI.WebSite.Data;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.IdentityUserTests
{
public class Bootstrap3AuthorizationTests : AuthorizationTests<Bootstrap3Startup, ApplicationDbContext>
public class Bootstrap3AuthorizationTests : AuthorizationTests<ApplicationUserStartup, ApplicationDbContext>
{
public Bootstrap3AuthorizationTests(ServerFactory<Bootstrap3Startup, ApplicationDbContext> serverFactory) : base(serverFactory)
public Bootstrap3AuthorizationTests(ServerFactory<ApplicationUserStartup, ApplicationDbContext> serverFactory) : base(serverFactory)
{
serverFactory.BootstrapFrameworkVersion = "V3";
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Identity.DefaultUI.WebSite;
@ -6,10 +6,11 @@ using Identity.DefaultUI.WebSite.Data;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Bootstrap3Tests
{
public class Bootstrap3ManagementTests : ManagementTests<Bootstrap3Startup, ApplicationDbContext>
public class Bootstrap3ManagementTests : ManagementTests<ApplicationUserStartup, ApplicationDbContext>
{
public Bootstrap3ManagementTests(ServerFactory<Bootstrap3Startup, ApplicationDbContext> serverFactory) : base(serverFactory)
public Bootstrap3ManagementTests(ServerFactory<ApplicationUserStartup, ApplicationDbContext> serverFactory) : base(serverFactory)
{
serverFactory.BootstrapFrameworkVersion = "V3";
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Identity.DefaultUI.WebSite;
@ -6,10 +6,11 @@ using Identity.DefaultUI.WebSite.Data;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Bootstrap3Tests
{
public class Bootstrap3RegistrationTests : RegistrationTests<Bootstrap3Startup, ApplicationDbContext>
public class Bootstrap3RegistrationTests : RegistrationTests<ApplicationUserStartup, ApplicationDbContext>
{
public Bootstrap3RegistrationTests(ServerFactory<Bootstrap3Startup, ApplicationDbContext> serverFactory) : base(serverFactory)
public Bootstrap3RegistrationTests(ServerFactory<ApplicationUserStartup, ApplicationDbContext> serverFactory) : base(serverFactory)
{
serverFactory.BootstrapFrameworkVersion = "V3";
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Identity.DefaultUI.WebSite;
@ -6,10 +6,11 @@ using Identity.DefaultUI.WebSite.Data;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Bootstrap3Tests
{
public class Bootstrap3LoginTests : LoginTests<Bootstrap3Startup, ApplicationDbContext>
public class Bootstrap3LoginTests : LoginTests<ApplicationUserStartup, ApplicationDbContext>
{
public Bootstrap3LoginTests(ServerFactory<Bootstrap3Startup, ApplicationDbContext> serverFactory) : base(serverFactory)
public Bootstrap3LoginTests(ServerFactory<ApplicationUserStartup, ApplicationDbContext> serverFactory) : base(serverFactory)
{
serverFactory.BootstrapFrameworkVersion = "V3";
}
}
}

View File

@ -2,19 +2,26 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Identity.DefaultUI.WebSite;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
namespace Microsoft.AspNetCore.Identity.FunctionalTests
{
public class ServerFactory<TStartup,TContext>: WebApplicationFactory<TStartup>
public class ServerFactory<TStartup, TContext> : WebApplicationFactory<TStartup>
where TStartup : class
where TContext : DbContext
{
@ -29,6 +36,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
ClientOptions.BaseAddress = new Uri("https://localhost");
}
public string BootstrapFrameworkVersion { get; set; } = "V4";
private bool IsHelixCI => typeof(ServerFactory<,>).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.Any(a => a.Key == "Microsoft.AspNetCore.Testing.IsHelixCI");
protected override IHostBuilder CreateHostBuilder()
{
Program.UseStartup = false;
@ -48,6 +59,70 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
// several places to pass important data in post-redirect-get flows.
.AddCookieTempDataProvider(o => o.Cookie.IsEssential = true);
});
UpdateStaticAssets(builder);
UpdateApplicationParts(builder);
}
private void UpdateApplicationParts(IWebHostBuilder builder) =>
builder.ConfigureServices(services => AddRelatedParts(services, BootstrapFrameworkVersion));
private void UpdateStaticAssets(IWebHostBuilder builder)
{
var manifestPath = Path.GetDirectoryName(new Uri(typeof(ServerFactory<,>).Assembly.CodeBase).LocalPath);
builder.ConfigureAppConfiguration((ctx, cb) =>
{
if (ctx.HostingEnvironment.WebRootFileProvider is CompositeFileProvider composite)
{
var originalWebRoot = composite.FileProviders.First();
ctx.HostingEnvironment.WebRootFileProvider = originalWebRoot;
}
});
string versionedPath = Path.Combine(manifestPath, $"Testing.DefaultWebSite.StaticWebAssets.{BootstrapFrameworkVersion}.xml");
UpdateManifest(versionedPath);
builder.ConfigureAppConfiguration((context, configBuilder) =>
{
using (var manifest = File.OpenRead(versionedPath))
{
typeof(StaticWebAssetsLoader)
.GetMethod("UseStaticWebAssetsCore", BindingFlags.NonPublic | BindingFlags.Static)
.Invoke(null, new object[] { context.HostingEnvironment, manifest });
}
});
}
private void UpdateManifest(string versionedPath)
{
var content = File.ReadAllText(versionedPath);
var path = typeof(ServerFactory<,>).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "Microsoft.AspNetCore.Testing.IdentityUIProjectPath").Value;
path = Directory.Exists(path) ? Path.Combine(path, "wwwroot") : Path.Combine(FindHelixSlnFileDirectory(), "UI", "wwwroot");
var updatedContent = content.Replace("{TEST_PLACEHOLDER}", path);
File.WriteAllText(versionedPath, updatedContent);
}
private string FindHelixSlnFileDirectory()
{
var applicationPath = Path.GetDirectoryName(typeof(ServerFactory<,>).Assembly.Location);
var directoryInfo = new DirectoryInfo(applicationPath);
do
{
var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "*.sln").FirstOrDefault();
if (solutionPath != null)
{
return directoryInfo.FullName;
}
directoryInfo = directoryInfo.Parent;
}
while (directoryInfo.Parent != null);
throw new InvalidOperationException($"Solution root could not be located using application root {applicationPath}.");
}
protected override IHost CreateHost(IHostBuilder builder)
@ -78,5 +153,76 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
base.Dispose(disposing);
}
private static void AddRelatedParts(IServiceCollection services, string framework)
{
var _assemblyMap =
new Dictionary<UIFramework, string>()
{
[UIFramework.Bootstrap3] = "Microsoft.AspNetCore.Identity.UI.Views.V3",
[UIFramework.Bootstrap4] = "Microsoft.AspNetCore.Identity.UI.Views.V4",
};
var mvcBuilder = services
.AddMvc()
.ConfigureApplicationPartManager(partManager =>
{
var thisAssembly = typeof(IdentityBuilderUIExtensions).Assembly;
var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(thisAssembly, throwOnError: true);
var relatedParts = relatedAssemblies.ToDictionary(
ra => ra,
CompiledRazorAssemblyApplicationPartFactory.GetDefaultApplicationParts);
var selectedFrameworkAssembly = _assemblyMap[framework == "V3" ? UIFramework.Bootstrap3 : UIFramework.Bootstrap4];
foreach (var kvp in relatedParts)
{
var assemblyName = kvp.Key.GetName().Name;
if (!IsAssemblyForFramework(selectedFrameworkAssembly, assemblyName))
{
RemoveParts(partManager, kvp.Value);
}
else
{
AddParts(partManager, kvp.Value);
}
}
bool IsAssemblyForFramework(string frameworkAssembly, string assemblyName) =>
string.Equals(assemblyName, frameworkAssembly, StringComparison.OrdinalIgnoreCase);
void RemoveParts(
ApplicationPartManager manager,
IEnumerable<ApplicationPart> partsToRemove)
{
for (var i = 0; i < manager.ApplicationParts.Count; i++)
{
var part = manager.ApplicationParts[i];
if (partsToRemove.Any(p => string.Equals(
p.Name,
part.Name,
StringComparison.OrdinalIgnoreCase)))
{
manager.ApplicationParts.Remove(part);
}
}
}
void AddParts(
ApplicationPartManager manager,
IEnumerable<ApplicationPart> partsToAdd)
{
foreach (var part in partsToAdd)
{
if (!manager.ApplicationParts.Any(p => p.GetType() == part.GetType() &&
string.Equals(p.Name, part.Name, StringComparison.OrdinalIgnoreCase)))
{
manager.ApplicationParts.Add(part);
}
}
}
});
}
}
}

View File

@ -7,9 +7,10 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Extensions.Core\src\Base32.cs" Link="Infrastructure\Base32.cs" />
<Compile Include="..\..\Extensions.Core\src\Rfc6238AuthenticationService.cs" Link="Infrastructure\Rfc6238AuthenticationService.cs" />
<Compile Include="..\..\UI\src\UIFramework.cs" Link="Infrastructure\UIFramework.cs" />
</ItemGroup>
<ItemGroup>
@ -27,6 +28,39 @@
<Reference Include="AngleSharp" />
</ItemGroup>
<ItemGroup>
<None Update="Testing.DefaultWebSite.StaticWebAssets.V4.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Testing.DefaultWebSite.StaticWebAssets.V4.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Testing.DefaultWebSite.StaticWebAssets.V3.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="AddProjectReferenceAssemblyInfo" BeforeTargets="GetAssemblyAttributes" DependsOnTargets="ResolveAssemblyReferences">
<ItemGroup>
<_IdentitUIDefaultWebSite Include="@(ReferencePath)" Condition="'%(ReferencePath.FileName)' == 'Identity.DefaultUI.WebSite'" />
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>Microsoft.AspNetCore.Testing.DefaultWebSiteProjectPath</_Parameter1>
<_Parameter2>$([System.IO.Path]::GetDirectoryName('%(_IdentitUIDefaultWebSite.MSBuildSourceProjectFile)'))</_Parameter2>
</AssemblyAttribute>
<_IdentitUIProjectPath Include="@(ReferencePath)" Condition="'%(ReferencePath.FileName)' == 'Microsoft.AspNetCore.Identity.UI'" />
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>Microsoft.AspNetCore.Testing.IdentityUIProjectPath</_Parameter1>
<_Parameter2>$([System.IO.Path]::GetDirectoryName('%(_IdentitUIProjectPath.MSBuildSourceProjectFile)'))</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute" Condition="'$(HelixType)' == 'ci'">
<_Parameter1>Microsoft.AspNetCore.Testing.IsHelixCI</_Parameter1>
<_Parameter2>true</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
<Target Name="PublishAssets" AfterTargets="Publish">
<ItemGroup>
<_PublishFiles Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Identity.UI\$(Configuration)\netcoreapp3.0\Microsoft.AspNetCore.Identity.UI.Views.*.dll" />
@ -34,22 +68,16 @@
<_PublishFiles Include="$(ArtifactsBinDir)Identity.DefaultUI.WebSite\$(Configuration)\netcoreapp3.0\Identity.DefaultUI.WebSite.deps.json" />
<_wwwrootFiles Include="$(MSBuildThisFileDirectory)..\..\testassets\Identity.DefaultUI.WebSite\wwwroot\**\*.*" />
<_PagesFiles Include="$(MSBuildThisFileDirectory)..\..\testassets\Identity.DefaultUI.WebSite\Pages\**\*.*" />
<_IdentityUIContent Include="$(MSBuildThisFileDirectory)..\..\UI\src\wwwroot\**\*" />
<_IdentityUIPages Include="$(MSBuildThisFileDirectory)..\..\UI\src\Areas\Identity\Pages\**\*" />
</ItemGroup>
<Copy
SourceFiles="@(_PublishFiles)"
DestinationFolder="$(PublishDir)" />
<Copy
SourceFiles="@(_PagesFiles)"
DestinationFolder="$(PublishDir)\Identity.DefaultUI.WebSite\Pages" />
<Copy
SourceFiles="@(_wwwrootFiles)"
DestinationFolder="$(PublishDir)\Identity.DefaultUI.WebSite\wwwroot" />
<Copy SourceFiles="@(_PublishFiles)" DestinationFolder="$(PublishDir)" />
<Copy SourceFiles="@(_PagesFiles)" DestinationFolder="$(PublishDir)\Identity.DefaultUI.WebSite\Pages" />
<Copy SourceFiles="@(_wwwrootFiles)" DestinationFolder="$(PublishDir)\Identity.DefaultUI.WebSite\wwwroot" />
<Copy SourceFiles="@(_IdentityUIContent)" DestinationFiles="$(PublishDir)\UI\wwwroot\%(_IdentityUIContent.RecursiveDir)\%(_IdentityUIContent.FileName)%(_IdentityUIContent.Extension)" />
<Copy SourceFiles="@(_IdentityUIPages)" DestinationFiles="$(PublishDir)UI\Areas\Identity\Pages\%(_IdentityUIPages.RecursiveDir)\%(_IdentityUIPages.FileName)%(_IdentityUIPages.Extension)" />
<!-- Drop a dummy sln to specify content root location -->
<WriteLinesToFile
File="$(PublishDir)\contentroot.sln"
Lines="Ignored"
Overwrite="true"
Encoding="Unicode" />
<WriteLinesToFile File="$(PublishDir)\contentroot.sln" Lines="Ignored" Overwrite="true" Encoding="Unicode" />
</Target>
</Project>

View File

@ -0,0 +1,3 @@
<StaticWebAssets Version="1.0">
<ContentRoot BasePath="/Identity" Path="{TEST_PLACEHOLDER}/V3" />
</StaticWebAssets>

View File

@ -0,0 +1,3 @@
<StaticWebAssets Version="1.0">
<ContentRoot BasePath="/Identity" Path="{TEST_PLACEHOLDER}/V4" />
</StaticWebAssets>

View File

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
using Xunit.Abstractions;
using System.Reflection;
namespace Microsoft.AspNetCore.Identity.Test
{
@ -83,11 +84,11 @@ namespace Microsoft.AspNetCore.Identity.Test
[Flaky("https://github.com/aspnet/AspNetCore-Internal/issues/2267", FlakyOn.AzP.macOS)]
public async Task IdentityUI_ScriptTags_FallbackSourceContent_Matches_CDNContent(ScriptTag scriptTag)
{
var wwwrootDir = Path.Combine(AppContext.BaseDirectory, "UI", "src", "wwwroot", scriptTag.Version);
var wwwrootDir = Path.Combine(GetProjectBasePath(), "wwwroot", scriptTag.Version);
var cdnContent = await _httpClient.GetStringAsync(scriptTag.Src);
var fallbackSrcContent = File.ReadAllText(
Path.Combine(wwwrootDir, scriptTag.FallbackSrc.TrimStart('~').TrimStart('/')));
Path.Combine(wwwrootDir, scriptTag.FallbackSrc.Replace("Identity", "").TrimStart('~').TrimStart('/')));
Assert.Equal(RemoveLineEndings(cdnContent), RemoveLineEndings(fallbackSrcContent));
}
@ -108,8 +109,8 @@ namespace Microsoft.AspNetCore.Identity.Test
private static List<ScriptTag> GetScriptTags()
{
var uiDirV3 = Path.Combine(AppContext.BaseDirectory, "UI", "src", "Areas", "Identity", "Pages", "V3");
var uiDirV4 = Path.Combine(AppContext.BaseDirectory, "UI", "src", "Areas", "Identity", "Pages", "V4");
var uiDirV3 = Path.Combine(GetProjectBasePath(), "Areas", "Identity", "Pages", "V3");
var uiDirV4 = Path.Combine(GetProjectBasePath(), "Areas", "Identity", "Pages", "V4");
var cshtmlFiles = GetRazorFiles(uiDirV3).Concat(GetRazorFiles(uiDirV4));
var scriptTags = new List<ScriptTag>();
@ -163,6 +164,32 @@ namespace Microsoft.AspNetCore.Identity.Test
_httpClient.Dispose();
}
private static string GetProjectBasePath()
{
var projectPath = typeof(IdentityUIScriptsTest).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "Microsoft.AspNetCore.Testing.DefaultUIProjectPath").Value;
return Directory.Exists(projectPath) ? projectPath : Path.Combine(FindHelixSlnFileDirectory(), "UI");
}
private static string FindHelixSlnFileDirectory()
{
var applicationPath = Path.GetDirectoryName(typeof(IdentityUIScriptsTest).Assembly.Location);
var directoryInfo = new DirectoryInfo(applicationPath);
do
{
var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "*.sln").FirstOrDefault();
if (solutionPath != null)
{
return directoryInfo.FullName;
}
directoryInfo = directoryInfo.Parent;
}
while (directoryInfo.Parent != null);
throw new InvalidOperationException($"Solution root could not be located using application root {applicationPath}.");
}
class RetryHandler : DelegatingHandler
{
public RetryHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

View File

@ -7,8 +7,6 @@
<ItemGroup>
<Compile Include="$(IdentityTestSharedSourceRoot)**\*.cs" />
<Compile Include="$(SharedSourceRoot)test\SkipOnHelixAttribute.cs" />
<Content Include="..\..\UI\src\Areas\Identity\Pages\**\*.*" LinkBase="UI\src\Areas\Identity\Pages" />
<Content Include="..\..\UI\src\wwwroot\**\*.*" LinkBase="UI\src\wwwroot" />
</ItemGroup>
<ItemGroup>
@ -17,8 +15,31 @@
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Identity.Specification.Tests" />
<Reference Include="Microsoft.AspNetCore.Identity" />
<Reference Include="Microsoft.AspNetCore.Identity.UI" />
<Reference Include="Microsoft.Extensions.Configuration" />
<Reference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
</ItemGroup>
<Target Name="AddProjectReferenceAssemblyInfo" BeforeTargets="GetAssemblyAttributes" DependsOnTargets="ResolveAssemblyReferences">
<ItemGroup>
<_IdentitUIDefaultUI Include="@(ReferencePath)" Condition="'%(ReferencePath.FileName)' == 'Microsoft.AspNetCore.Identity.UI'" />
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>Microsoft.AspNetCore.Testing.DefaultUIProjectPath</_Parameter1>
<_Parameter2>$([System.IO.Path]::GetDirectoryName('%(_IdentitUIDefaultUI.MSBuildSourceProjectFile)'))</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
<Target Name="PublishAssets" AfterTargets="Publish">
<ItemGroup>
<_IdentityUIContent Include="$(MSBuildThisFileDirectory)..\..\UI\src\wwwroot\**\*" />
<_IdentityUIPages Include="$(MSBuildThisFileDirectory)..\..\UI\src\Areas\Identity\Pages\**\*" />
</ItemGroup>
<Copy SourceFiles="@(_IdentityUIContent)" DestinationFiles="$(PublishDir)\UI\wwwroot\%(_IdentityUIContent.RecursiveDir)\%(_IdentityUIContent.FileName)%(_IdentityUIContent.Extension)" />
<Copy SourceFiles="@(_IdentityUIPages)" DestinationFiles="$(PublishDir)UI\Areas\Identity\Pages\%(_IdentityUIPages.RecursiveDir)\%(_IdentityUIPages.FileName)%(_IdentityUIPages.Extension)" />
<!-- Drop a dummy sln to specify content root location -->
<WriteLinesToFile File="$(PublishDir)\contentroot.sln" Lines="Ignored" Overwrite="true" Encoding="Unicode" />
</Target>
</Project>

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Identity.DefaultUI.WebSite
{
public class Bootstrap3Startup : ApplicationUserStartup
{
public Bootstrap3Startup(IConfiguration configuration) : base(configuration)
{
}
public override UIFramework Framework => UIFramework.Bootstrap3;
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UserSecretsId>aspnet-Identity.DefaultUI.WebSite-80C658D8-CED7-467F-9B47-75DA3BC1A16D</UserSecretsId>
<IdentityDefaultUIFramework>Bootstrap3</IdentityDefaultUIFramework>
</PropertyGroup>
<ItemGroup>
@ -43,4 +44,15 @@
<Reference Include="Microsoft.Extensions.Logging.Debug" />
</ItemGroup>
<PropertyGroup>
<ResolveStaticWebAssetsInputsDependsOn>_SetBootstrapFrameworkVersion</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<Target Name="_SetBootstrapFrameworkVersion">
<ItemGroup>
<_StaticWebAssetsProjectReference Condition="'%(FileName)' == 'Microsoft.AspNetCore.Identity.UI'">
<AdditionalProperties>IdentityDefaultUIFramework=$(IdentityDefaultUIFramework)</AdditionalProperties>
</_StaticWebAssetsProjectReference>
</ItemGroup>
</Target>
</Project>

View File

@ -27,7 +27,6 @@ namespace Identity.DefaultUI.WebSite
});
services.AddDefaultIdentity<Microsoft.AspNetCore.Identity.Test.PocoUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddUserManager<UserManager<Microsoft.AspNetCore.Identity.Test.PocoUser>>();
services.AddSingleton<IUserStore<Microsoft.AspNetCore.Identity.Test.PocoUser>, InMemoryUserStore<Microsoft.AspNetCore.Identity.Test.PocoUser>>();

View File

@ -28,8 +28,6 @@ namespace Identity.DefaultUI.WebSite
public IConfiguration Configuration { get; }
public virtual UIFramework Framework { get; } = UIFramework.Bootstrap4;
// This method gets called by the runtime. Use this method to add services to the container.
public virtual void ConfigureServices(IServiceCollection services)
{
@ -49,7 +47,6 @@ namespace Identity.DefaultUI.WebSite
.UseSqlite("DataSource=:memory:"));
services.AddDefaultIdentity<TUser>()
.AddDefaultUI(Framework)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<TContext>();

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -41,11 +42,19 @@ namespace Templates.Test.Helpers
public ITestOutputHelper Output { get; set; }
public IMessageSink DiagnosticsMessageSink { get; set; }
internal async Task<ProcessEx> RunDotNetNewAsync(string templateName, string auth = null, string language = null, bool useLocalDB = false, bool noHttps = false, string[] args = null)
internal async Task<ProcessEx> RunDotNetNewAsync(
string templateName,
string auth = null,
string language = null,
bool useLocalDB = false,
bool noHttps = false,
string[] args = null,
// Used to set special options in MSBuild
IDictionary<string, string> environmentVariables = null)
{
var hiveArg = $"--debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
var argString = $"new {templateName} {hiveArg}";
environmentVariables ??= new Dictionary<string, string>();
if (!string.IsNullOrEmpty(auth))
{
argString += $" --auth {auth}";
@ -86,7 +95,7 @@ namespace Templates.Test.Helpers
await DotNetNewLock.WaitAsync();
try
{
var execution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString);
var execution = ProcessEx.Run(Output, AppContext.BaseDirectory, DotNetMuxer.MuxerPathOrDefault(), argString, environmentVariables);
await execution.Exited;
return execution;
}
@ -96,7 +105,7 @@ namespace Templates.Test.Helpers
}
}
internal async Task<ProcessEx> RunDotNetPublishAsync(bool takeNodeLock = false)
internal async Task<ProcessEx> RunDotNetPublishAsync(bool takeNodeLock = false, IDictionary<string,string> packageOptions = null)
{
Output.WriteLine("Publishing ASP.NET application...");
@ -112,7 +121,7 @@ namespace Templates.Test.Helpers
await effectiveLock.WaitAsync();
try
{
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release {extraArgs}");
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release {extraArgs}", packageOptions);
await result.Exited;
return result;
}
@ -122,7 +131,7 @@ namespace Templates.Test.Helpers
}
}
internal async Task<ProcessEx> RunDotNetBuildAsync(bool takeNodeLock = false)
internal async Task<ProcessEx> RunDotNetBuildAsync(bool takeNodeLock = false, IDictionary<string,string> packageOptions = null)
{
Output.WriteLine("Building ASP.NET application...");
@ -133,7 +142,7 @@ namespace Templates.Test.Helpers
await effectiveLock.WaitAsync();
try
{
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), "build -c Debug");
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), "build -c Debug", packageOptions);
await result.Exited;
return result;
}

View File

@ -0,0 +1,190 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Templates.Test.Helpers;
using Xunit;
using Xunit.Abstractions;
namespace Templates.Test
{
public class IdentityUIPackageTest
{
public IdentityUIPackageTest(ProjectFactoryFixture projectFactory, ITestOutputHelper output)
{
ProjectFactory = projectFactory;
Output = output;
}
public Project Project { get; set; }
public ProjectFactoryFixture ProjectFactory { get; set; }
public ITestOutputHelper Output { get; }
public static TheoryData<IDictionary<string, string>, string, string[]> MSBuildIdentityUIPackageOptions
{
get
{
var data = new TheoryData<IDictionary<string, string>, string, string[]>();
data.Add(new Dictionary<string, string>
{
["IdentityUIFrameworkVersion"] = "Bootstrap3"
},
"Bootstrap v3.4.1",
Bootstrap3ContentFiles);
data.Add(new Dictionary<string, string>(), "Bootstrap v4.3.1", Bootstrap4ContentFiles);
return data;
}
}
public static string[] Bootstrap3ContentFiles { get; } = new string[]
{
"Identity/css/site.css",
"Identity/js/site.js",
"Identity/lib/bootstrap/dist/css/bootstrap-theme.css",
"Identity/lib/bootstrap/dist/css/bootstrap-theme.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap-theme.min.css",
"Identity/lib/bootstrap/dist/css/bootstrap-theme.min.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap.css",
"Identity/lib/bootstrap/dist/css/bootstrap.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap.min.css",
"Identity/lib/bootstrap/dist/css/bootstrap.min.css.map",
"Identity/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
"Identity/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
"Identity/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
"Identity/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
"Identity/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2",
"Identity/lib/bootstrap/dist/js/bootstrap.js",
"Identity/lib/bootstrap/dist/js/bootstrap.min.js",
"Identity/lib/bootstrap/dist/js/npm.js",
"Identity/lib/jquery/LICENSE.txt",
"Identity/lib/jquery/dist/jquery.js",
"Identity/lib/jquery/dist/jquery.min.js",
"Identity/lib/jquery/dist/jquery.min.map",
"Identity/lib/jquery-validation/LICENSE.md",
"Identity/lib/jquery-validation/dist/additional-methods.js",
"Identity/lib/jquery-validation/dist/additional-methods.min.js",
"Identity/lib/jquery-validation/dist/jquery.validate.js",
"Identity/lib/jquery-validation/dist/jquery.validate.min.js",
"Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js",
"Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
"Identity/lib/jquery-validation-unobtrusive/LICENSE.txt",
};
public static string[] Bootstrap4ContentFiles { get; } = new string[]
{
"Identity/favicon.ico",
"Identity/css/site.css",
"Identity/js/site.js",
"Identity/lib/bootstrap/dist/css/bootstrap-grid.css",
"Identity/lib/bootstrap/dist/css/bootstrap-grid.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap-grid.min.css",
"Identity/lib/bootstrap/dist/css/bootstrap-grid.min.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap-reboot.css",
"Identity/lib/bootstrap/dist/css/bootstrap-reboot.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap-reboot.min.css",
"Identity/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap.css",
"Identity/lib/bootstrap/dist/css/bootstrap.css.map",
"Identity/lib/bootstrap/dist/css/bootstrap.min.css",
"Identity/lib/bootstrap/dist/css/bootstrap.min.css.map",
"Identity/lib/bootstrap/dist/js/bootstrap.bundle.js",
"Identity/lib/bootstrap/dist/js/bootstrap.bundle.js.map",
"Identity/lib/bootstrap/dist/js/bootstrap.bundle.min.js",
"Identity/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map",
"Identity/lib/bootstrap/dist/js/bootstrap.js",
"Identity/lib/bootstrap/dist/js/bootstrap.js.map",
"Identity/lib/bootstrap/dist/js/bootstrap.min.js",
"Identity/lib/bootstrap/dist/js/bootstrap.min.js.map",
"Identity/lib/jquery/LICENSE.txt",
"Identity/lib/jquery/dist/jquery.js",
"Identity/lib/jquery/dist/jquery.min.js",
"Identity/lib/jquery/dist/jquery.min.map",
"Identity/lib/jquery-validation/LICENSE.md",
"Identity/lib/jquery-validation/dist/additional-methods.js",
"Identity/lib/jquery-validation/dist/additional-methods.min.js",
"Identity/lib/jquery-validation/dist/jquery.validate.js",
"Identity/lib/jquery-validation/dist/jquery.validate.min.js",
"Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js",
"Identity/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
"Identity/lib/jquery-validation-unobtrusive/LICENSE.txt",
};
[Theory]
[MemberData(nameof(MSBuildIdentityUIPackageOptions))]
public async Task IdentityUIPackage_WorksWithDifferentOptions(IDictionary<string, string> packageOptions, string versionValidator, string[] expectedFiles)
{
Project = await ProjectFactory.GetOrCreateProject("identityuipackage" + string.Concat(packageOptions.Values), Output);
var useLocalDB = false;
var createResult = await Project.RunDotNetNewAsync("razor", auth: "Individual", useLocalDB: useLocalDB, environmentVariables: packageOptions);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
var projectFileContents = ReadFile(Project.TemplateOutputDir, $"{Project.ProjectName}.csproj");
Assert.Contains(".db", projectFileContents);
var publishResult = await Project.RunDotNetPublishAsync(packageOptions: packageOptions);
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
// Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
// The output from publish will go into bin/Release/netcoreapp3.0/publish and won't be affected by calling build
// later, while the opposite is not true.
var buildResult = await Project.RunDotNetBuildAsync(packageOptions: packageOptions);
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync("razorpages");
Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult));
Project.AssertEmptyMigration("razorpages");
using (var aspNetProcess = Project.StartBuiltProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
var response = await aspNetProcess.SendRequest("/Identity/lib/bootstrap/dist/css/bootstrap.css");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(versionValidator, await response.Content.ReadAsStringAsync());
await ValidatePublishedFiles(aspNetProcess, expectedFiles);
}
using (var aspNetProcess = Project.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", Project, aspNetProcess.Process));
var response = await aspNetProcess.SendRequest("/Identity/lib/bootstrap/dist/css/bootstrap.css");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(versionValidator, await response.Content.ReadAsStringAsync());
await ValidatePublishedFiles(aspNetProcess, expectedFiles);
}
}
private async Task ValidatePublishedFiles(AspNetProcess aspNetProcess, string[] expectedContentFiles)
{
foreach (var file in expectedContentFiles)
{
var response = await aspNetProcess.SendRequest(file);
Assert.True(response?.StatusCode == HttpStatusCode.OK, $"Couldn't find file '{file}'");
}
}
private string ReadFile(string basePath, string path)
{
var fullPath = Path.Combine(basePath, path);
var doesExist = File.Exists(fullPath);
Assert.True(doesExist, $"Expected file to exist, but it doesn't: {path}");
return File.ReadAllText(Path.Combine(basePath, path));
}
}
}