Skip to content
Draft
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 @@ -39,6 +39,15 @@ public IEnumerable<string> GetProjectCapabilities(bool refresh = false)
return [];
}

/// <summary>
/// Gets an evaluated project property, including values supplied by imported props and targets.
/// </summary>
public string? GetPropertyValue(string propertyName)
{
EnsureInitialized();
return _project?.GetPropertyValue(propertyName);
}

private void Initialize(bool refresh = false)
{
lock (_initLock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ public Type[] GetScaffoldSteps()
typeof(AddAspNetConnectionStringStep),
typeof(AddDbSetToExistingContextStep),
typeof(AddFileStep),
typeof(AddIdentityMigrationStep),
typeof(AreaScaffolderStep),
typeof(ConfigureIdentityNavigationStep),
typeof(DetectBlazorWasmStep),
typeof(DotnetNewScaffolderStep),
typeof(EmptyControllerScaffolderStep),
typeof(IdentityCodeModificationStep),
typeof(NuGetVersionService),
typeof(RegisterAppStep),
typeof(UpdateAppAuthorizationStep),
Expand Down Expand Up @@ -342,7 +345,9 @@ public void AddScaffolderCommands()
.WithIdentityDbContextStep()
.WithAspNetConnectionStringStep()
.WithIdentityTextTemplatingStep()
.WithIdentityCodeChangeStep();
.WithIdentityCodeChangeStep()
.WithIdentityNavigationStep()
.WithIdentityMigrationStep();

_builder.AddScaffolder(ScaffolderCatagory.AspNet, AspnetStrings.EntraId.Name)
.WithDisplayName(AspnetStrings.EntraId.DisplayName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ internal static ProjectInfo GetProjectInfo(string projectPath, ILogger logger)
ProjectInfo projectInfo = new(projectPath)
{
CodeService = codeService,
ProjectAssetsFile = msBuildProject.GetPropertyValue("ProjectAssetsFile"),
Capabilities = capabilities
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public ProjectInfo(string? projectPath)
/// Gets or sets the code service for the project.
/// </summary>
public CodeService? CodeService { get; set; }
public string? ProjectAssetsFile { get; set; }
/// <summary>
/// Gets or sets the list of code change options for the project.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static IScaffoldBuilder WithIdentityAddPackagesStep(this IScaffoldBuilder
List<Package> packages = [
PackageConstants.AspNetCorePackages.AspNetCoreIdentityEfPackage,
PackageConstants.AspNetCorePackages.AspNetCoreIdentityUiPackage,
PackageConstants.AspNetCorePackages.AspNetCoreDiagnosticsEfCorePackage,
PackageConstants.EfConstants.EfCoreToolsPackage,
PackageConstants.EfConstants.EfCoreDesignPackage
];
Expand Down Expand Up @@ -86,14 +87,21 @@ public static IScaffoldBuilder WithIdentityTextTemplatingStep(this IScaffoldBuil
//all the .cshtml and their model class (.cshtml.cs) templates
var allIdentityPageFiles = templateFolderUtilities.GetAllT4TemplatesForTargetFramework(["Identity"], identityModel.ProjectInfo.ProjectPath);
//ApplicationUser.tt template
var applicationUserFile = templateFolderUtilities.GetAllT4TemplatesForTargetFramework(["Files"], identityModel.ProjectInfo.ProjectPath)
var fileTemplates = templateFolderUtilities.GetAllT4TemplatesForTargetFramework(["Files"], identityModel.ProjectInfo.ProjectPath).ToList();
var applicationUserFile = fileTemplates
.FirstOrDefault(x => x.EndsWith("ApplicationUser.tt", StringComparison.OrdinalIgnoreCase));
var identityFileProperties = IdentityHelper.GetTextTemplatingProperties(allIdentityPageFiles, identityModel);
var applicationUserProperty = IdentityHelper.GetApplicationUserTextTemplatingProperty(applicationUserFile, identityModel);
if (applicationUserProperty is not null)
{
identityFileProperties = identityFileProperties.Append(applicationUserProperty);
}
var loginPartialTemplate = fileTemplates.FirstOrDefault(x => x.EndsWith("_LoginPartial.tt", StringComparison.OrdinalIgnoreCase));
var loginPartialProperty = IdentityHelper.GetLoginPartialTextTemplatingProperty(loginPartialTemplate, identityModel);
if (loginPartialProperty is not null)
{
identityFileProperties = identityFileProperties.Append(loginPartialProperty);
}

if (identityFileProperties is not null && identityFileProperties.Any())
{
Expand All @@ -118,7 +126,7 @@ public static IScaffoldBuilder WithIdentityTextTemplatingStep(this IScaffoldBuil
/// <returns>The updated scaffold builder.</returns>
public static IScaffoldBuilder WithIdentityCodeChangeStep(this IScaffoldBuilder builder)
{
builder = builder.WithStep<WrappedCodeModificationStep>(config =>
builder = builder.WithStep<IdentityCodeModificationStep>(config =>
{
var step = config.Step;
//get needed properties and cast them as needed
Expand All @@ -142,7 +150,7 @@ codeModifierProperties is not null &&
{
step.CodeModifierProperties.TryAdd(kvp.Key, kvp.Value);
}

step.CodeService = identityModel.ProjectInfo.CodeService!;
step.ProjectPath = identitySettings.Project;
step.CodeChangeOptions = identityModel.ProjectInfo.CodeChangeOptions ?? [];
}
Expand All @@ -155,4 +163,52 @@ codeModifierProperties is not null &&

return builder;
}

/// <summary>
/// Adds a step to configure Identity navigation in the host application's layout.
/// </summary>
/// <param name="builder">The scaffold builder.</param>
/// <returns>The updated scaffold builder.</returns>
public static IScaffoldBuilder WithIdentityNavigationStep(this IScaffoldBuilder builder)
{
return builder.WithStep<ConfigureIdentityNavigationStep>(config =>
{
var step = config.Step;
if (config.Context.Properties.TryGetValue(nameof(IdentityModel), out var identityModelObj) &&
identityModelObj is IdentityModel identityModel)
{
step.ProjectPath = identityModel.ProjectInfo.ProjectPath ?? string.Empty;
step.IsRazorPages = identityModel.IsRazorPages;
}
else
{
step.SkipStep = true;
}
});
}

/// <summary>
/// Adds a step to generate an initial EF Core migration for Identity.
/// </summary>
/// <param name="builder">The scaffold builder.</param>
/// <returns>The updated scaffold builder.</returns>
public static IScaffoldBuilder WithIdentityMigrationStep(this IScaffoldBuilder builder)
{
return builder.WithStep<AddIdentityMigrationStep>(config =>
{
var step = config.Step;
if (config.Context.Properties.TryGetValue(nameof(IdentityModel), out var identityModelObj) &&
identityModelObj is IdentityModel identityModel)
{
step.ProjectPath = identityModel.ProjectInfo.ProjectPath ?? string.Empty;
step.DbContextName = identityModel.DbContextInfo.DbContextClassName ?? string.Empty;
step.ProjectAssetsFile = identityModel.ProjectInfo.ProjectAssetsFile ?? string.Empty;
step.SkipStep = identityModel.HasMigration;
}
else
{
step.SkipStep = true;
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.DotNet.Scaffolding.Core.Model;
using Microsoft.DotNet.Scaffolding.Internal;
using Microsoft.DotNet.Scaffolding.TextTemplating;
using Microsoft.DotNet.Tools.Scaffold.AspNet.Common;
using Microsoft.DotNet.Tools.Scaffold.AspNet.Models;

namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Helpers;
Expand All @@ -12,6 +15,11 @@ namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Helpers;
/// </summary>
internal static class IdentityHelper
{
internal static InvocationExpressionSyntax? FindIdentityRegistration(SyntaxNode root)
=> root.DescendantNodes().OfType<InvocationExpressionSyntax>().FirstOrDefault(call =>
call.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax name } &&
name.Identifier.ValueText is "AddDefaultIdentity" or "AddIdentity" or "AddIdentityCore");

/// <summary>
/// Use the template paths and IdentityModel to create valid 'TextTemplateProperty' objects.
/// </summary>
Expand All @@ -36,8 +44,7 @@ internal static IEnumerable<TextTemplatingProperty> GetTextTemplatingProperties(
x.FullName.Contains(templateFullName) &&
x.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));

var projectName = Path.GetFileNameWithoutExtension(identityModel.ProjectInfo.ProjectPath);
if (!string.IsNullOrEmpty(templatePath) && templateType is not null && !string.IsNullOrEmpty(projectName))
if (!string.IsNullOrEmpty(templatePath) && templateType is not null)
{
string extension = string.Empty;
//the 'ManageNavPagesModel.tt' only should have .cs extension (only exception)
Expand All @@ -50,9 +57,11 @@ internal static IEnumerable<TextTemplatingProperty> GetTextTemplatingProperties(
extension = templateFullName.EndsWith("Model", StringComparison.OrdinalIgnoreCase) ? ".cshtml.cs" : ".cshtml";
}

string formattedTemplateName = templateFullName.Replace("Model", string.Empty, StringComparison.OrdinalIgnoreCase);
string templateNameWithNamespace = $"{identityModel.IdentityNamespace}.{formattedTemplateName}";
string outputFileName = $"{StringUtil.ToPath(templateNameWithNamespace, identityModel.BaseOutputPath, projectName)}{extension}";
var name = templateFullName.EndsWith("Model", StringComparison.OrdinalIgnoreCase)
? templateFullName[..^"Model".Length]
: templateFullName;
var outputFileName = Path.Combine(identityModel.BaseOutputPath, "Areas", "Identity",
name.Replace('.', Path.DirectorySeparatorChar)) + extension;
textTemplatingProperties.Add(new()
{
TemplateModel = identityModel,
Expand All @@ -75,7 +84,7 @@ internal static IEnumerable<TextTemplatingProperty> GetTextTemplatingProperties(
private static string GetFormattedRelativeIdentityFile(string fullFileName)
{
string identifier = $"Identity{Path.DirectorySeparatorChar}";
int index = fullFileName.IndexOf(identifier);
int index = fullFileName.LastIndexOf(identifier, StringComparison.Ordinal);
if (index != -1)
{
string pathAfterIdentifier = fullFileName.Substring(index + identifier.Length);
Expand All @@ -95,7 +104,7 @@ private static string GetFormattedRelativeIdentityFile(string fullFileName)
internal static TextTemplatingProperty? GetApplicationUserTextTemplatingProperty(string? applicationUserTemplate, IdentityModel identityModel)
{
var projectDirectory = Path.GetDirectoryName(identityModel.ProjectInfo.ProjectPath);
if (string.IsNullOrEmpty(applicationUserTemplate) || string.IsNullOrEmpty(projectDirectory))
if (identityModel.HasExistingUser || string.IsNullOrEmpty(applicationUserTemplate) || string.IsNullOrEmpty(projectDirectory))
{
return null;
}
Expand All @@ -117,6 +126,51 @@ private static string GetFormattedRelativeIdentityFile(string fullFileName)
};
}

internal static TextTemplatingProperty? GetLoginPartialTextTemplatingProperty(string? templatePath, IdentityModel model)
{
var projectDirectory = Path.GetDirectoryName(model.ProjectInfo.ProjectPath);
if (string.IsNullOrEmpty(templatePath) || string.IsNullOrEmpty(projectDirectory))
{
return null;
}

var outputPath = Path.Combine(projectDirectory, model.IsRazorPages ? "Pages" : "Views", "Shared", "_LoginPartial.cshtml");
if (File.Exists(outputPath))
{
return null;
}

return new TextTemplatingProperty
{
TemplatePath = templatePath,
TemplateType = typeof(Templates.net10.Files._LoginPartial),
TemplateModel = model,
TemplateModelName = "Model",
OutputPath = outputPath
};
}

internal static bool HasMigration(IEnumerable<ISymbol> classes, DbContextInfo context)
=> classes.OfType<INamedTypeSymbol>().Any(type =>
type.BaseType?.Name == "ModelSnapshot" &&
type.GetAttributes().Any(attribute =>
attribute.AttributeClass?.Name == "DbContextAttribute" &&
attribute.ConstructorArguments.FirstOrDefault().Value is INamedTypeSymbol dbContext &&
dbContext.Name == context.DbContextClassName &&
(dbContext.ContainingNamespace.IsGlobalNamespace ? string.Empty : dbContext.ContainingNamespace.ToDisplayString()) == context.DbContextNamespace));

internal static ITypeSymbol? GetIdentityUserType(INamedTypeSymbol? dbContext)
{
for (var type = dbContext; type is not null; type = type.BaseType)
{
if (type.Name is "IdentityDbContext" or "IdentityUserContext" && type.TypeArguments.Length > 0)
{
return type.TypeArguments[0];
}
}
return null;
}

private static IList<Type> GetIdentityTemplateTypes(TargetFramework? targetFramework)
{
return targetFramework switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ internal class IdentityModel
/// Used to determine the correct layout path in _ViewStart.cshtml.
/// </summary>
public bool IsRazorPages { get; set; }
public bool HasMigration { get; set; }
public bool HasExistingUser { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ public AddAspNetConnectionStringStep(
public override Task<bool> ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default)
{
var appSettingsFileSearch = _fileSystem.EnumerateFiles(BaseProjectPath, "appsettings.json", SearchOption.AllDirectories);
var appSettingsFile = appSettingsFileSearch.FirstOrDefault();
var appSettingsFile = appSettingsFileSearch.FirstOrDefault() ?? Path.Combine(BaseProjectPath, "appsettings.json");
JsonNode? content;
bool writeContent = false;
if (string.IsNullOrEmpty(appSettingsFile) || !_fileSystem.FileExists(appSettingsFile))
if (!_fileSystem.FileExists(appSettingsFile))
{
content = new JsonObject();
writeContent = true;
Expand Down Expand Up @@ -97,7 +97,7 @@ connectionStringObject[ConnectionStringName] is null &&
content[connectionStringNodeName] = connectionStringObject;
}

if (writeContent && !string.IsNullOrEmpty(appSettingsFile))
if (writeContent)
{
var options = new JsonSerializerOptions { WriteIndented = true };
_fileSystem.WriteAllText(appSettingsFile, content.ToJsonString(options));
Expand Down
Loading
Loading