From 69242bb8f392bc5863dac1f44d566ef9ea9e8860 Mon Sep 17 00:00:00 2001 From: haileymck Date: Tue, 9 Dec 2025 13:18:16 -0800 Subject: [PATCH 1/8] do not have target framework param --- .../ComponentModel/ParameterHelpers.cs | 5 - .../NuGet/TargetFrameworkConstants.cs | 8 +- .../ScaffolderContextExtensions.cs | 13 ++- .../AspNet/AspNetCommandService.cs | 58 ++---------- .../AspNet/Commands/AspNetOptions.cs | 11 --- .../AspNet/Common/ClassAnalyzers.cs | 11 +-- .../AspNet/Common/ProjectInfo.cs | 94 ++++++++++++++++++- .../ScaffoldSteps/ValidateBlazorCrudStep.cs | 8 +- .../ScaffoldSteps/ValidateEfControllerStep.cs | 8 +- .../ScaffoldSteps/ValidateEntraIdStep.cs | 7 +- .../ScaffoldSteps/ValidateIdentityStep.cs | 8 +- .../ScaffoldSteps/ValidateMinimalApiStep.cs | 8 +- .../ScaffoldSteps/ValidateRazorPagesStep.cs | 8 +- .../AspNet/ScaffoldSteps/ValidateViewsStep.cs | 8 +- .../Flow/Steps/ParameterBasedFlowStep.cs | 10 +- .../ValidateTargetFrameworkStep.cs | 55 ----------- .../dotnet-scaffold/dotnet-scaffold.csproj | 1 + 17 files changed, 153 insertions(+), 168 deletions(-) delete mode 100644 src/dotnet-scaffolding/dotnet-scaffold/ScaffoldingSteps/ValidateTargetFrameworkStep.cs diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/ParameterHelpers.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/ParameterHelpers.cs index c4b6b1cd6..bb3819b79 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/ParameterHelpers.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/ParameterHelpers.cs @@ -43,9 +43,4 @@ private static bool CanConvertToType(string value, Type type) return false; } } - - public static bool IsTargetFrameworkOption(Parameter parameter) - { - return string.Equals(parameter.DisplayName, Model.TargetFrameworkConstants.TargetFrameworkDisplayName, StringComparison.Ordinal); - } } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs index e21a6dd73..a9de084da 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs @@ -1,19 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; - namespace Microsoft.DotNet.Scaffolding.Core.Model; internal static class TargetFrameworkConstants { - public const string TargetFrameworkCliOption = "--framework"; - public const string TargetFrameworkDisplayName = "Target Framework"; - public const string TargetFrameworkDescription = "Specifies the target framework for the scaffolded project."; + public const string TargetFrameworkPropertyName = "TargetFramework"; public const string Net8 = "net8.0"; public const string Net9 = "net9.0"; public const string Net10 = "net10.0"; - - public static readonly ImmutableArray SupportedTargetFrameworks = [ Net8, Net9, Net10]; } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Scaffolders/ScaffolderContextExtensions.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Scaffolders/ScaffolderContextExtensions.cs index ab136979f..c0ac1d499 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Scaffolders/ScaffolderContextExtensions.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Scaffolders/ScaffolderContextExtensions.cs @@ -16,11 +16,18 @@ public static class ScaffolderContextExtensions /// The target framework name if present; otherwise, null. public static string? GetSpecifiedTargetFramework(this ScaffolderContext context) { - if (context.GetOptionResult(Model.TargetFrameworkConstants.TargetFrameworkCliOption) is string tfm) + string? targetFramework = null; + if (context.Properties.TryGetValue(TargetFrameworkConstants.TargetFrameworkPropertyName, out object? tfm)) { - return tfm; + targetFramework = tfm as string; } - return null; + return targetFramework; + } + + public static string? SetSpecifiedTargetFramework(this ScaffolderContext context, string? targetFramework) + { + context.Properties[TargetFrameworkConstants.TargetFrameworkPropertyName] = targetFramework; + return targetFramework; } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs index ea3d83b60..49932a58b 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/AspNetCommandService.cs @@ -11,7 +11,6 @@ using Microsoft.DotNet.Tools.Scaffold.AspNet.ScaffoldSteps; using Microsoft.DotNet.Tools.Scaffold.AspNet.ScaffoldSteps.Settings; using Microsoft.DotNet.Tools.Scaffold.Command; -using Microsoft.DotNet.Tools.Scaffold.ScaffoldingSteps; namespace Microsoft.DotNet.Tools.Scaffold.AspNet { @@ -40,7 +39,6 @@ public Type[] GetScaffoldSteps() typeof(ValidateIdentityStep), typeof(ValidateMinimalApiStep), typeof(ValidateRazorPagesStep), - typeof(ValidateTargetFrameworkStep), typeof(ValidateViewsStep), typeof(WrappedAddPackagesStep), typeof(WrappedCodeModificationStep), @@ -132,13 +130,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.Api.ApiControllerCrudDisplayName) .WithCategory(AspnetStrings.Catagories.API) .WithDescription(AspnetStrings.Api.ApiControllerCrudDescription) - .WithOptions([options.Project, options.ModelName, options.ControllerName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.ModelName, options.ControllerName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -161,13 +153,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.MVC.CrudDisplayName) .WithCategory(AspnetStrings.Catagories.MVC) .WithDescription(AspnetStrings.MVC.CrudDescription) - .WithOptions([options.Project, options.ModelName, options.ControllerName, options.Views, options.DataContextClassRequired, options.DatabaseProviderRequired, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.ModelName, options.ControllerName, options.Views, options.DataContextClassRequired, options.DatabaseProviderRequired, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -191,13 +177,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.Blazor.CrudDisplayName) .WithCategory(AspnetStrings.Catagories.Blazor) .WithDescription(AspnetStrings.Blazor.CrudDescription) - .WithOptions([options.Project, options.ModelName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.PageType, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.ModelName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.PageType, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -219,13 +199,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.RazorPage.CrudDisplayName) .WithCategory(AspnetStrings.Catagories.RazorPages) .WithDescription(AspnetStrings.RazorPage.CrudDescription) - .WithOptions([options.Project, options.ModelName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.PageType, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.ModelName, options.DataContextClassRequired, options.DatabaseProviderRequired, options.PageType, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -263,13 +237,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.Api.MinimalApiDisplayName) .WithCategory(AspnetStrings.Catagories.API) .WithDescription(AspnetStrings.Api.MinimalApiDescription) - .WithOptions([options.Project, options.ModelName, options.EndpointsClass, options.OpenApi, options.DataContextClass, options.DatabaseProvider, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.ModelName, options.EndpointsClass, options.OpenApi, options.DataContextClass, options.DatabaseProvider, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -306,13 +274,7 @@ public void AddScaffolderCommands() .WithCategory(AspnetStrings.Catagories.Blazor) .WithCategory(AspnetStrings.Catagories.Identity) .WithDescription(AspnetStrings.Blazor.IdentityDescription) - .WithOptions([options.Project, options.DataContextClassRequired, options.IdentityDbProviderRequired, options.Overwrite, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.DataContextClassRequired, options.IdentityDbProviderRequired, options.Overwrite, options.Prerelease]) .WithStep(config => { var step = config.Step; @@ -335,13 +297,7 @@ public void AddScaffolderCommands() .WithDisplayName(AspnetStrings.Identity.DisplayName) .WithCategory(AspnetStrings.Catagories.Identity) .WithDescription(AspnetStrings.Identity.Description) - .WithOptions([options.Project, options.DataContextClassRequired, options.IdentityDbProviderRequired, options.Overwrite, options.TargetFramework, options.Prerelease]) - .WithStep(config => - { - var step = config.Step; - var context = config.Context; - step.TargetFramework = context.GetOptionResult(options.TargetFramework); - }) + .WithOptions([options.Project, options.DataContextClassRequired, options.IdentityDbProviderRequired, options.Overwrite, options.Prerelease]) .WithStep(config => { var step = config.Step; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs index 9545e7c51..165103270 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Commands/AspNetOptions.cs @@ -29,7 +29,6 @@ internal class AspNetOptions public ScaffolderOption Views { get; } public ScaffolderOption Overwrite { get; } public ScaffolderOption Application { get; } - public ScaffolderOption TargetFramework { get; } private ScaffolderOption? _username = null; private ScaffolderOption? _tenantId = null; @@ -193,16 +192,6 @@ public AspNetOptions() PickerType = InteractivePickerType.ConditionalPicker, CustomPickerValues = AspnetStrings.Options.Application.Values }; - - TargetFramework = new ScaffolderOption - { - DisplayName = Scaffolding.Core.Model.TargetFrameworkConstants.TargetFrameworkDisplayName, - CliOption = Scaffolding.Core.Model.TargetFrameworkConstants.TargetFrameworkCliOption, - Description = Scaffolding.Core.Model.TargetFrameworkConstants.TargetFrameworkDescription, - Required = false, - PickerType = InteractivePickerType.CustomPicker, - CustomPickerValues = Scaffolding.Core.Model.TargetFrameworkConstants.SupportedTargetFrameworks - }; } public ScaffolderOption Username => _username ??= new() diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ClassAnalyzers.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ClassAnalyzers.cs index 2e7ffbf80..f00bcf6a1 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ClassAnalyzers.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ClassAnalyzers.cs @@ -187,15 +187,12 @@ internal static ProjectInfo GetProjectInfo(string projectPath, ILogger logger) * unlike ICodeService, also no chance for a wasted op because at this point in the scaffolder, * we will definitely to have MSBuild initialized.*/ new MsBuildInitializer(logger).Initialize(); - var codeService = new CodeService(logger, projectPath); - var msBuildProject = new MSBuildProjectService(projectPath); - var lowestTFM = msBuildProject.GetLowestTargetFramework(); - var capabilities = msBuildProject.GetProjectCapabilities().ToList(); - var projectInfo = new ProjectInfo() + CodeService codeService = new(logger, projectPath); + MSBuildProjectService msBuildProject = new(projectPath); + List capabilities = msBuildProject.GetProjectCapabilities().ToList(); + ProjectInfo projectInfo = new(projectPath) { CodeService = codeService, - ProjectPath = projectPath, - LowestTargetFramework = lowestTFM, Capabilities = capabilities }; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index 60bdb7e50..ad1e0d68b 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -1,5 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; +using Microsoft.DotNet.Scaffolding.Internal.CliHelpers; using Microsoft.DotNet.Scaffolding.Roslyn.Services; namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Common; @@ -9,10 +11,16 @@ namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Common; /// internal class ProjectInfo { + public ProjectInfo(string? projectPath) + { + ProjectPath = projectPath; + LowestTargetFramework = projectPath is not null ? GetLowestTargetFrameworkFromCli(projectPath) : null; + } + /// /// Gets or sets the path to the project file. /// - public string? ProjectPath { get; set; } + public string? ProjectPath { get; } /// /// Gets or sets the code service for the project. /// @@ -24,9 +32,91 @@ internal class ProjectInfo /// /// Gets or sets the lowest target framework for the project (if multiple are found). /// - public string? LowestTargetFramework { get; set; } + public string? LowestTargetFramework { get; } /// /// Gets or sets the list of project capabilities. /// public IList? Capabilities { get; set; } + + /// + /// Gets the lowest target framework from the project using dotnet msbuild CLI command. + /// + /// The path to the project file. + /// The lowest target framework moniker (TFM) as a string, or null if not found. + private static string? GetLowestTargetFrameworkFromCli(string projectPath) + { + //Should I only care about net 8 and above???? + + var runner = DotnetCliRunner.CreateDotNet("msbuild", new[] { "-getProperty:TargetFramework;TargetFrameworks", projectPath }); + int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); + + if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) + { + return null; + } + + // Parse the JSON output + try + { + var msbuildOutput = JsonSerializer.Deserialize(stdOut); + if (msbuildOutput?.Properties == null) + { + return null; + } + + // If single TargetFramework is set, return it + if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFramework)) + { + return msbuildOutput.Properties.TargetFramework; + } + + // If multiple TargetFrameworks are set, find the lowest version + if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFrameworks)) + { + var frameworks = msbuildOutput.Properties.TargetFrameworks + .Split(';') + .Where(x => x.StartsWith("net") || x.StartsWith("netstandard")) + .OrderBy(ParseFrameworkVersion) + .ToList(); + + return frameworks.FirstOrDefault(); + } + + return null; + } + catch (JsonException) + { + return null; + } + + static Version ParseFrameworkVersion(string tfm) + { + // Remove "net" or "netstandard" prefix to parse the version + string versionPart = tfm.StartsWith("netstandard") ? + tfm.Replace("netstandard", "") : + tfm.Replace("net", ""); + + // Parse to Version; assume "0.0" for invalid formats + return Version.TryParse(versionPart, out var version) ? version : new Version(0, 0); + } + } + + /// + /// Represents the output from dotnet msbuild -getProperty command. + /// + private class MsBuildPropertiesOutput + { + public MsBuildProperties? Properties { get; set; } + } + + /// + /// Represents the properties returned from dotnet msbuild -getProperty command. + /// + private class MsBuildProperties + { + public string? TargetFramework { get; set; } + public string? TargetFrameworks { get; set; } + } + + } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs index 9f44795b9..2bb3954c3 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs @@ -91,7 +91,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell //initialize MinimalApiModel _logger.LogInformation("Initializing scaffolding model..."); - var blazorCrudModel = await GetBlazorCrudModelAsync(blazorCrudSettings); + var blazorCrudModel = await GetBlazorCrudModelAsync(context, blazorCrudSettings); if (blazorCrudModel is null) { _logger.LogError("An error occurred."); @@ -197,11 +197,13 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the BlazorCrudModel for scaffolding. /// + /// The scaffolder context. /// The CRUD settings containing user input. /// A task representing the asynchronous operation, with a result containing the BlazorCrudModel, or null if initialization fails. - private async Task GetBlazorCrudModelAsync(CrudSettings settings) + private async Task GetBlazorCrudModelAsync(ScaffolderContext context, CrudSettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs index d8f1ff5b6..33f69acd0 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs @@ -93,7 +93,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell //initialize CrudControllerModel _logger.LogInformation("Initializing scaffolding model..."); - var efControllerModel = await GetEfControllerModelAsync(efControllerSettings); + var efControllerModel = await GetEfControllerModelAsync(context, efControllerSettings); if (efControllerModel is null) { _logger.LogError("An error occurred."); @@ -206,10 +206,12 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// Initializes and returns the EfControllerModel for scaffolding. /// /// EF Controller settings. + /// Scaffolder context. /// Initialized EfControllerModel. - private async Task GetEfControllerModelAsync(EfControllerSettings settings) + private async Task GetEfControllerModelAsync(ScaffolderContext context, EfControllerSettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs index c25f5452c..fe3fc5261 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs @@ -81,7 +81,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell } _logger.LogInformation("Initializing Entra ID scaffolding model..."); - var entraIdModel = await GetEntraIdModelAsync(entraIdSettings); + var entraIdModel = await GetEntraIdModelAsync(context, entraIdSettings); if (entraIdModel is null) { @@ -157,7 +157,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the EntraIdModel for scaffolding. /// - private async Task GetEntraIdModelAsync(EntraIdSettings settings) + private async Task GetEntraIdModelAsync(ScaffolderContext context, EntraIdSettings settings) { if (string.IsNullOrEmpty(settings.Project)) { @@ -165,7 +165,8 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell return null; } - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo?.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs index 0691fefcb..7949aeca7 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs @@ -90,7 +90,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell //initialize IdentityModel _logger.LogInformation("Initializing scaffolding model..."); - var identityModel = await GetIdentityModelAsync(identitySettings); + var identityModel = await GetIdentityModelAsync(context, identitySettings); if (identityModel is null) { _logger.LogError("An error occurred."); @@ -181,11 +181,13 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the IdentityModel for scaffolding. /// + /// The ScaffolderContext for the current operation. /// The IdentitySettings used to initialize the model. /// A task that represents the asynchronous operation, with a result of the IdentityModel. - private async Task GetIdentityModelAsync(IdentitySettings settings) + private async Task GetIdentityModelAsync(ScaffolderContext context, IdentitySettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs index 9de01c372..beb8df14a 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs @@ -94,7 +94,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell //initialize MinimalApiModel _logger.LogInformation("Initializing scaffolding model..."); - var minimalApiModel = await GetMinimalApiModelAsync(minimalApiSettings); + var minimalApiModel = await GetMinimalApiModelAsync(context, minimalApiSettings); if (minimalApiModel is null) { _logger.LogError("An error occurred."); @@ -188,11 +188,13 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the MinimalApiModel for scaffolding. /// + /// ScaffolderContext object containing the context for the scaffolding. /// MinimalApiSettings object containing the settings for the scaffolding. /// Task containing the MinimalApiModel or null if initialization fails. - private async Task GetMinimalApiModelAsync(MinimalApiSettings settings) + private async Task GetMinimalApiModelAsync(ScaffolderContext context, MinimalApiSettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs index b65fc10ba..48711d749 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs @@ -89,7 +89,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell //initialize RazorPageModel _logger.LogInformation("Initializing scaffolding model..."); - var razorPageModel = await GetRazorPageModelAsync(razorPagesSettings); + var razorPageModel = await GetRazorPageModelAsync(context, razorPagesSettings); if (razorPageModel is null) { _logger.LogError("An error occurred."); @@ -187,11 +187,13 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the RazorPageModel for scaffolding. /// + /// The scaffolder context. /// The validated CRUD settings. /// A task that represents the asynchronous operation, with the RazorPageModel as the result. - private async Task GetRazorPageModelAsync(CrudSettings settings) + private async Task GetRazorPageModelAsync(ScaffolderContext context, CrudSettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs index a2bdb9b11..780153bb7 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs @@ -72,7 +72,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell context.Properties.Add(nameof(CrudSettings), viewSettings); } - var viewModel = await GetViewModelAsync(viewSettings); + var viewModel = await GetViewModelAsync(context, viewSettings); if (viewModel is null) { _logger.LogError("An error occurred: 'ViewModel' instance could not be obtained"); @@ -128,11 +128,13 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell /// /// Initializes and returns the ViewModel for scaffolding. /// + /// Scaffolder context. /// CrudSettings object containing the settings for scaffolding. /// Task that represents the asynchronous operation, with a ViewModel result if successful, null otherwise. - private async Task GetViewModelAsync(CrudSettings settings) + private async Task GetViewModelAsync(ScaffolderContext context, CrudSettings settings) { - var projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); + context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs index 8b3c15502..85c1f3f5b 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs @@ -81,12 +81,10 @@ public async ValueTask RunAsync(IFlowContext context, Cancellati { NextStep = NextStep?.NextStep; } - else if (ParameterHelpers.IsTargetFrameworkOption(Parameter) && !string.Equals(parameterValue, TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase)) - { - // Skip the prerelease step if the target framework is not net10, prerelease only applies to net10 - //TODO update for the next major release of .NET - NextStep = NextStep?.NextStep; - } + // if the next step is prerelease option and the target framework is not net 10, skip the prerelease option + //TODO update with each major release + // Need to know target framework before here + if (NextStep != null) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/ScaffoldingSteps/ValidateTargetFrameworkStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/ScaffoldingSteps/ValidateTargetFrameworkStep.cs deleted file mode 100644 index 3f627cbcc..000000000 --- a/src/dotnet-scaffolding/dotnet-scaffold/ScaffoldingSteps/ValidateTargetFrameworkStep.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.DotNet.Scaffolding.Core.Scaffolders; -using Microsoft.DotNet.Scaffolding.Core.Steps; -using Microsoft.DotNet.Scaffolding.Core.Model; -using Microsoft.Extensions.Logging; - -namespace Microsoft.DotNet.Tools.Scaffold.ScaffoldingSteps; - -public class ValidateTargetFrameworkStep : ScaffoldStep -{ - private readonly ILogger _logger; - - /// - /// Gets or sets the target .NET framework for the project or component. - /// - /// Specify the framework using a valid framework moniker, such as "net6.0" or - /// "netstandard2.1". This property is typically used to determine compatibility and runtime behavior. - public string? TargetFramework { get; set; } - - public ValidateTargetFrameworkStep(ILogger logger) - { - _logger = logger; - } - - public override Task ExecuteAsync(ScaffolderContext context, CancellationToken cancellationToken = default) - { - if (!string.IsNullOrEmpty(TargetFramework) && !ValidateTargetFrameworkOption(TargetFramework, _logger)) - { - return Task.FromResult(false); - } - return Task.FromResult(true); - } - - private static bool ValidateTargetFrameworkOption(string? value, ILogger logger) - { - if (string.IsNullOrWhiteSpace(value)) - { - // Option is optional, so do not error if not specified - return true; - } - - string normalizedValue = value.Trim().ToLowerInvariant(); - - // Check if it's a valid supported framework - if (!TargetFrameworkConstants.SupportedTargetFrameworks.Contains(normalizedValue)) - { - logger.LogError($"Invalid {TargetFrameworkConstants.TargetFrameworkCliOption} option: '{value}'. Must be a valid .NET SDK version, net8.0, net9.0 or net10.0."); - return false; - } - - return true; - } -} diff --git a/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj b/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj index 075a7424b..76cdee82e 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj +++ b/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj @@ -691,4 +691,5 @@ + From 5b7dd0faa64d3f5dfc3e146b213332c0d1706386 Mon Sep 17 00:00:00 2001 From: haileymck Date: Wed, 10 Dec 2025 14:52:34 -0800 Subject: [PATCH 2/8] Skip Prerelease option if target framework is not net 10 --- .../Builder/ScaffolderOptionOfT.cs | 2 +- .../ComponentModel/Parameter.cs | 14 +++++++ .../AspNet/Common/ProjectInfo.cs | 4 +- .../Interactive/Flow/FlowContextProperties.cs | 2 + .../Flow/Steps/ParameterBasedFlowStep.cs | 39 +++++++++++++++++-- .../dotnet-scaffold/dotnet-scaffold.csproj | 1 - 6 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Builder/ScaffolderOptionOfT.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Builder/ScaffolderOptionOfT.cs index c1c586d83..8cbb32226 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Builder/ScaffolderOptionOfT.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Builder/ScaffolderOptionOfT.cs @@ -52,5 +52,5 @@ internal override Parameter ToParameter() /// /// Gets the normalized CLI option name. /// - private string FixedName => CliOption ?? $"--{DisplayName.ToLowerInvariant().Replace(" ", "-")}"; + private string FixedName => Parameter.GetParameterName(CliOption, DisplayName); } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/Parameter.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/Parameter.cs index 634ecb1a7..99e8f221c 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/Parameter.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/ComponentModel/Parameter.cs @@ -91,4 +91,18 @@ public static CliTypes GetCliType(Type type) /// The corresponding CLI type. public static CliTypes GetCliType() => GetCliType(typeof(T)); + + + /// + /// Generates the command-line parameter name based on the specified option or display name. + /// + /// The explicit command-line option name to use. If not specified, the parameter name is generated from . + /// The display name used to generate the command-line parameter name if is null. + /// A string containing the command-line parameter name. If is not null, its value is + /// returned; otherwise, a name is generated from . + internal static string GetParameterName(string? cliOption, string displayName) + { + return cliOption ?? $"--{displayName.ToLowerInvariant().Replace(" ", "-")}"; + } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index ad1e0d68b..f88000525 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -43,10 +43,8 @@ public ProjectInfo(string? projectPath) /// /// The path to the project file. /// The lowest target framework moniker (TFM) as a string, or null if not found. - private static string? GetLowestTargetFrameworkFromCli(string projectPath) + internal static string? GetLowestTargetFrameworkFromCli(string projectPath) { - //Should I only care about net 8 and above???? - var runner = DotnetCliRunner.CreateDotNet("msbuild", new[] { "-getProperty:TargetFramework;TargetFrameworks", projectPath }); int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/FlowContextProperties.cs b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/FlowContextProperties.cs index 8505a4e29..681ece9e9 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/FlowContextProperties.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/FlowContextProperties.cs @@ -57,4 +57,6 @@ internal static class FlowContextProperties public const string ChosenCategory = nameof(ChosenCategory); /// Key for telemetry environment variables dictionary. public const string TelemetryEnvironmentVariables = nameof(TelemetryEnvironmentVariables); + + public const string ProjectFileParameterResult = nameof(ProjectFileParameterResult); } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs index 85c1f3f5b..84cfde49d 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs @@ -5,6 +5,8 @@ using Microsoft.DotNet.Scaffolding.Core.Model; using Microsoft.DotNet.Scaffolding.Internal.Services; using Microsoft.DotNet.Scaffolding.Roslyn.Services; +using Microsoft.DotNet.Tools.Scaffold.AspNet.Commands; +using Microsoft.DotNet.Tools.Scaffold.AspNet.Common; using Microsoft.Extensions.Logging; using Spectre.Console.Flow; @@ -81,10 +83,11 @@ public async ValueTask RunAsync(IFlowContext context, Cancellati { NextStep = NextStep?.NextStep; } - // if the next step is prerelease option and the target framework is not net 10, skip the prerelease option - //TODO update with each major release - // Need to know target framework before here - + + if (NextStep is not null && NextStep.Parameter.DisplayName.Equals(AspnetStrings.Options.Prerelease.DisplayName, StringComparison.Ordinal) && ShouldSkipPrereleaseOption(context)) + { + NextStep = NextStep?.NextStep; + } if (NextStep != null) { @@ -165,5 +168,33 @@ private void SelectCodeService(IFlowContext context, string projectPath) codeService)); } } + + /// + /// Determines whether the prerelease option should be skipped based on the target framework of the current + /// project. + /// + /// The prerelease option is skipped for projects targeting frameworks other than .NET + /// 10. If the project file or target framework cannot be determined, the prerelease option is not + /// skipped. + /// The flow context containing project information and properties. Must not be null. + /// true if the prerelease option should be skipped for the current project; otherwise, false. + private bool ShouldSkipPrereleaseOption(IFlowContext context) + { + //TODO update with each major release of .NET + + string projectParameterKey = Parameter.GetParameterName(Constants.CliOptions.ProjectCliOption, AspnetStrings.Options.Project.DisplayName); + + if (context.Properties.Get(projectParameterKey) is FlowProperty projectFileProperty && + projectFileProperty.Value is string projectFilePath && !string.IsNullOrEmpty(projectFilePath)) + { + string? targetFramework = ProjectInfo.GetLowestTargetFrameworkFromCli(projectFilePath); + if (!string.IsNullOrEmpty(targetFramework) && !targetFramework.Equals(TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase)) + { + // skip the prerelease step for non-net10 projects + return true; + } + } + return false; + } } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj b/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj index 76cdee82e..075a7424b 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj +++ b/src/dotnet-scaffolding/dotnet-scaffold/dotnet-scaffold.csproj @@ -691,5 +691,4 @@ - From 9492ac575b20ff578100c6a950499514a8d775aa Mon Sep 17 00:00:00 2001 From: haileymck Date: Thu, 11 Dec 2025 11:55:08 -0800 Subject: [PATCH 3/8] log instead of throw when adding packages with unsupported tfm --- .../NuGet/Package.cs | 16 +++++++++------- .../Steps/AddPackagesStep.cs | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs index 25ce9d709..40cf8159b 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; using NuGet.Versioning; namespace Microsoft.DotNet.Scaffolding.Core.Model; @@ -28,15 +29,16 @@ internal static class PackageExtensions /// The package instance for which to resolve the version. Must not be null. /// The target framework identifier used to determine the appropriate package version. For example, "net6.0". /// The NuGet version helper to use for version resolution. + /// The logger to use for logging messages. /// A package instance with the version property set to the resolved version for the specified target framework, or /// the original package if the version is already set or cannot be resolved. - public static async Task WithResolvedVersionAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper) + public static async Task WithResolvedVersionAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) { if (package.PackageVersion is not null) { return package; } - NuGetVersion? resolvedVersion = await package.GetVersionForTargetFrameworkAsync(targetFramework, nugetVersionHelper); + NuGetVersion? resolvedVersion = await package.GetVersionForTargetFrameworkAsync(targetFramework, nugetVersionHelper, logger); if (resolvedVersion is null) { return package; @@ -54,11 +56,10 @@ public static async Task WithResolvedVersionAsync(this Package package, /// The target framework identifier (for example, "net8.0", "net9.0", or "net10.0") for which the package version is /// requested. Case-insensitive. /// The NuGet version helper to use for version resolution. + /// The logger to use for logging messages. /// A task that represents the asynchronous operation. The task result contains the corresponding NuGet package - /// version if available; otherwise, if the package does not require a version. - /// Thrown if is not one of the supported frameworks ("net8.0", "net9.0", or - /// "net10.0"). - private static Task GetVersionForTargetFrameworkAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper) + /// version if available; otherwise, if the package does not require a version or target framework is not supported. + private static Task GetVersionForTargetFrameworkAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) { if (!package.IsVersionRequired) { @@ -79,7 +80,8 @@ public static async Task WithResolvedVersionAsync(this Package package, } else { - throw new NotSupportedException($"Target framework '{targetFramework}' is not supported."); + logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing recent release of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); + return Task.FromResult(null); } } } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs index 26592fa7a..028c3dbea 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs @@ -58,7 +58,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell Package resolvedPackage = package; if (package.IsVersionRequired && !string.IsNullOrEmpty(targetFramework) && !Prerelease) { - resolvedPackage = await package.WithResolvedVersionAsync(targetFramework, _nugetVersionHelper); + resolvedPackage = await package.WithResolvedVersionAsync(targetFramework, _nugetVersionHelper, _logger); packageVersion = resolvedPackage.PackageVersion; } From 148f41a4fa91c8d77bb20e2f1fc1a5cded95ff5e Mon Sep 17 00:00:00 2001 From: haileymck <111816896+haileymck@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:37:26 -0800 Subject: [PATCH 4/8] call msbuild to verify target framework version --- .../NuGet/Package.cs | 2 +- .../CliHelpers/DotnetCliRunner.cs | 6 +- .../CliHelpers/MsBuildCliRunner.cs | 38 +++++++++ .../AspNet/Common/ProjectInfo.cs | 81 +++++++++++++------ 4 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs index 40cf8159b..2dacd1eca 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs @@ -80,7 +80,7 @@ public static async Task WithResolvedVersionAsync(this Package package, } else { - logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing recent release of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); + logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing latest stable version of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); return Task.FromResult(null); } } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs index bd07433a9..19fe75658 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Diagnostics; -using System.Text; namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; @@ -11,9 +9,11 @@ namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; /// internal class DotnetCliRunner { + private const string DotnetCommandName = "dotnet"; + public static DotnetCliRunner CreateDotNet(string commandName, IEnumerable args, IDictionary? environmentVariables = null) { - return Create("dotnet", new[] { commandName }.Concat(args), environmentVariables); + return Create(DotnetCommandName, new[] { commandName }.Concat(args), environmentVariables); } public static DotnetCliRunner Create(string commandName, IEnumerable args, IDictionary? environmentVariables = null) diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs new file mode 100644 index 000000000..8b79c8f1e --- /dev/null +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; + +internal class MsBuildCliRunner +{ + private const string MsbuildCommandName = "msbuild"; + + /// + /// Executes an MSBuild command and deserializes the JSON output into the specified type. + /// + /// The type to deserialize the JSON output into. + /// The arguments to pass to the MSBuild command. + /// The path to the project file. + /// The deserialized object of type T, or null if deserialization fails. + public static T? RunMSBuildCommandAndDeserialize(IEnumerable args, string projectPath) where T : class + { + try + { + var runner = DotnetCliRunner.CreateDotNet(MsbuildCommandName, args.Append(projectPath)); + int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); + + if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) + { + return null; + } + + return JsonSerializer.Deserialize(stdOut); + } + catch (JsonException) + { + return null; + } + } +} diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index f88000525..c8bc44fef 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -45,24 +45,15 @@ public ProjectInfo(string? projectPath) /// The lowest target framework moniker (TFM) as a string, or null if not found. internal static string? GetLowestTargetFrameworkFromCli(string projectPath) { - var runner = DotnetCliRunner.CreateDotNet("msbuild", new[] { "-getProperty:TargetFramework;TargetFrameworks", projectPath }); - int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); - - if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) - { - return null; - } - - // Parse the JSON output try { - var msbuildOutput = JsonSerializer.Deserialize(stdOut); - if (msbuildOutput?.Properties == null) + MsBuildPropertiesOutput? msbuildOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize(["-getProperty:TargetFramework;TargetFrameworks"], projectPath); + if (msbuildOutput?.Properties is null) { return null; } - // If single TargetFramework is set, return it + // If a single TargetFramework is set, return it if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFramework)) { return msbuildOutput.Properties.TargetFramework; @@ -71,13 +62,10 @@ public ProjectInfo(string? projectPath) // If multiple TargetFrameworks are set, find the lowest version if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFrameworks)) { - var frameworks = msbuildOutput.Properties.TargetFrameworks - .Split(';') - .Where(x => x.StartsWith("net") || x.StartsWith("netstandard")) - .OrderBy(ParseFrameworkVersion) - .ToList(); + string[] frameworks = msbuildOutput.Properties.TargetFrameworks.Split(';'); + string? lowestFramework = GetLowestFromFrameworks(frameworks, projectPath); - return frameworks.FirstOrDefault(); + return lowestFramework; } return null; @@ -87,15 +75,41 @@ public ProjectInfo(string? projectPath) return null; } - static Version ParseFrameworkVersion(string tfm) + static string? GetLowestFromFrameworks(string[] frameworks, string projectPath) { - // Remove "net" or "netstandard" prefix to parse the version - string versionPart = tfm.StartsWith("netstandard") ? - tfm.Replace("netstandard", "") : - tfm.Replace("net", ""); + List<(string tfm, Version version)> targetFrameworks = []; + foreach (string tfm in frameworks) + { + try + { + MsBuildFrameworkOutput? frameworkOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize([$"-p:TargetFramework=\"{tfm}\"", + "-getProperty:TargetFrameworkIdentifier;TargetFrameworkVersion"], + projectPath); - // Parse to Version; assume "0.0" for invalid formats - return Version.TryParse(versionPart, out var version) ? version : new Version(0, 0); + if (frameworkOutput?.Properties?.TargetFrameworkVersion is not null) + { + string version = frameworkOutput.Properties.TargetFrameworkVersion.TrimStart('v'); + if (Version.TryParse(version, out var tfmVersion)) + { + targetFrameworks.Add((tfm, tfmVersion)); + } + } + } + catch (JsonException) + { + continue; + } + } + + if (targetFrameworks.Count == 0) + { + return null; + } + + return targetFrameworks + .OrderBy(f => f.version) + .Select(f => f.tfm) + .FirstOrDefault(); } } @@ -116,5 +130,20 @@ private class MsBuildProperties public string? TargetFrameworks { get; set; } } - + /// + /// Represents the output from dotnet msbuild -getProperty command for framework identifiers. + /// + private class MsBuildFrameworkOutput + { + public MsBuildFrameworkProperties? Properties { get; set; } + } + + /// + /// Represents the framework properties returned from dotnet msbuild -getProperty command. + /// + private class MsBuildFrameworkProperties + { + public string? TargetFrameworkIdentifier { get; set; } + public string? TargetFrameworkVersion { get; set; } + } } From d3f0591cca387e1fe099b1c15f22f1960738bdc5 Mon Sep 17 00:00:00 2001 From: haileymck Date: Tue, 16 Dec 2025 12:02:53 -0800 Subject: [PATCH 5/8] do not get TargetFrameworkIdentifier property from msbuild --- .../CliHelpers/MsBuildCliRunner.cs | 28 +++++++++++++++---- .../AspNet/Common/ProjectInfo.cs | 27 +++--------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs index 8b79c8f1e..b5ae47ebc 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs @@ -20,19 +20,35 @@ internal class MsBuildCliRunner { try { - var runner = DotnetCliRunner.CreateDotNet(MsbuildCommandName, args.Append(projectPath)); - int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); - - if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) + if (RunMsBuildCommand(args, projectPath) is string stdOut) { - return null; + return JsonSerializer.Deserialize(stdOut); } - return JsonSerializer.Deserialize(stdOut); + return null; } catch (JsonException) { return null; } } + + /// + /// Runs an MSBuild command with the specified arguments and project path, and returns the output as a string. + /// + /// The collection of command-line arguments to pass to MSBuild. Each string represents a single argument. + /// The full path to the project file to build. Cannot be null or empty. + /// A string containing the output from the MSBuild command, or null if the command fails or produces no output. + public static string? RunMSBuildCommandAndGetOutput(IEnumerable args, string projectPath) + { + return RunMsBuildCommand(args, projectPath); + } + + private static string? RunMsBuildCommand(IEnumerable args, string projectPath) + { + DotnetCliRunner runner = DotnetCliRunner.CreateDotNet(MsbuildCommandName, args.Append(projectPath)); + int exitCode = runner.ExecuteAndCaptureOutput(out string? stdOut, out string? stdErr); + + return exitCode != 0 || string.IsNullOrEmpty(stdOut) || !string.IsNullOrEmpty(stdErr) ? null : stdOut; + } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index c8bc44fef..c0b7ffd2d 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -82,13 +82,11 @@ public ProjectInfo(string? projectPath) { try { - MsBuildFrameworkOutput? frameworkOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize([$"-p:TargetFramework=\"{tfm}\"", - "-getProperty:TargetFrameworkIdentifier;TargetFrameworkVersion"], - projectPath); - - if (frameworkOutput?.Properties?.TargetFrameworkVersion is not null) + if (MsBuildCliRunner.RunMSBuildCommandAndGetOutput([$"-p:TargetFramework=\"{tfm}\"", + "-getProperty:TargetFrameworkVersion"], + projectPath) is string frameworkOutput) { - string version = frameworkOutput.Properties.TargetFrameworkVersion.TrimStart('v'); + string version = frameworkOutput.TrimStart('v'); if (Version.TryParse(version, out var tfmVersion)) { targetFrameworks.Add((tfm, tfmVersion)); @@ -129,21 +127,4 @@ private class MsBuildProperties public string? TargetFramework { get; set; } public string? TargetFrameworks { get; set; } } - - /// - /// Represents the output from dotnet msbuild -getProperty command for framework identifiers. - /// - private class MsBuildFrameworkOutput - { - public MsBuildFrameworkProperties? Properties { get; set; } - } - - /// - /// Represents the framework properties returned from dotnet msbuild -getProperty command. - /// - private class MsBuildFrameworkProperties - { - public string? TargetFrameworkIdentifier { get; set; } - public string? TargetFrameworkVersion { get; set; } - } } From 3dd2eb6ab0a9d88945c78a987b8a68e681431393 Mon Sep 17 00:00:00 2001 From: haileymck <111816896+haileymck@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:37:26 -0800 Subject: [PATCH 6/8] call msbuild to verify target framework version This reverts commit d3f0591cca387e1fe099b1c15f22f1960738bdc5. --- .../NuGet/Package.cs | 2 +- .../CliHelpers/DotnetCliRunner.cs | 6 +- .../CliHelpers/MsBuildCliRunner.cs | 38 +++++++++ .../AspNet/Common/ProjectInfo.cs | 81 +++++++++++++------ 4 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs index 40cf8159b..2dacd1eca 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs @@ -80,7 +80,7 @@ public static async Task WithResolvedVersionAsync(this Package package, } else { - logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing recent release of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); + logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing latest stable version of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); return Task.FromResult(null); } } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs index bd07433a9..19fe75658 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/DotnetCliRunner.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Diagnostics; -using System.Text; namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; @@ -11,9 +9,11 @@ namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; /// internal class DotnetCliRunner { + private const string DotnetCommandName = "dotnet"; + public static DotnetCliRunner CreateDotNet(string commandName, IEnumerable args, IDictionary? environmentVariables = null) { - return Create("dotnet", new[] { commandName }.Concat(args), environmentVariables); + return Create(DotnetCommandName, new[] { commandName }.Concat(args), environmentVariables); } public static DotnetCliRunner Create(string commandName, IEnumerable args, IDictionary? environmentVariables = null) diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs new file mode 100644 index 000000000..8b79c8f1e --- /dev/null +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Internal/CliHelpers/MsBuildCliRunner.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.DotNet.Scaffolding.Internal.CliHelpers; + +internal class MsBuildCliRunner +{ + private const string MsbuildCommandName = "msbuild"; + + /// + /// Executes an MSBuild command and deserializes the JSON output into the specified type. + /// + /// The type to deserialize the JSON output into. + /// The arguments to pass to the MSBuild command. + /// The path to the project file. + /// The deserialized object of type T, or null if deserialization fails. + public static T? RunMSBuildCommandAndDeserialize(IEnumerable args, string projectPath) where T : class + { + try + { + var runner = DotnetCliRunner.CreateDotNet(MsbuildCommandName, args.Append(projectPath)); + int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); + + if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) + { + return null; + } + + return JsonSerializer.Deserialize(stdOut); + } + catch (JsonException) + { + return null; + } + } +} diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index f88000525..c8bc44fef 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -45,24 +45,15 @@ public ProjectInfo(string? projectPath) /// The lowest target framework moniker (TFM) as a string, or null if not found. internal static string? GetLowestTargetFrameworkFromCli(string projectPath) { - var runner = DotnetCliRunner.CreateDotNet("msbuild", new[] { "-getProperty:TargetFramework;TargetFrameworks", projectPath }); - int exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); - - if (exitCode != 0 || string.IsNullOrEmpty(stdOut)) - { - return null; - } - - // Parse the JSON output try { - var msbuildOutput = JsonSerializer.Deserialize(stdOut); - if (msbuildOutput?.Properties == null) + MsBuildPropertiesOutput? msbuildOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize(["-getProperty:TargetFramework;TargetFrameworks"], projectPath); + if (msbuildOutput?.Properties is null) { return null; } - // If single TargetFramework is set, return it + // If a single TargetFramework is set, return it if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFramework)) { return msbuildOutput.Properties.TargetFramework; @@ -71,13 +62,10 @@ public ProjectInfo(string? projectPath) // If multiple TargetFrameworks are set, find the lowest version if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFrameworks)) { - var frameworks = msbuildOutput.Properties.TargetFrameworks - .Split(';') - .Where(x => x.StartsWith("net") || x.StartsWith("netstandard")) - .OrderBy(ParseFrameworkVersion) - .ToList(); + string[] frameworks = msbuildOutput.Properties.TargetFrameworks.Split(';'); + string? lowestFramework = GetLowestFromFrameworks(frameworks, projectPath); - return frameworks.FirstOrDefault(); + return lowestFramework; } return null; @@ -87,15 +75,41 @@ public ProjectInfo(string? projectPath) return null; } - static Version ParseFrameworkVersion(string tfm) + static string? GetLowestFromFrameworks(string[] frameworks, string projectPath) { - // Remove "net" or "netstandard" prefix to parse the version - string versionPart = tfm.StartsWith("netstandard") ? - tfm.Replace("netstandard", "") : - tfm.Replace("net", ""); + List<(string tfm, Version version)> targetFrameworks = []; + foreach (string tfm in frameworks) + { + try + { + MsBuildFrameworkOutput? frameworkOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize([$"-p:TargetFramework=\"{tfm}\"", + "-getProperty:TargetFrameworkIdentifier;TargetFrameworkVersion"], + projectPath); - // Parse to Version; assume "0.0" for invalid formats - return Version.TryParse(versionPart, out var version) ? version : new Version(0, 0); + if (frameworkOutput?.Properties?.TargetFrameworkVersion is not null) + { + string version = frameworkOutput.Properties.TargetFrameworkVersion.TrimStart('v'); + if (Version.TryParse(version, out var tfmVersion)) + { + targetFrameworks.Add((tfm, tfmVersion)); + } + } + } + catch (JsonException) + { + continue; + } + } + + if (targetFrameworks.Count == 0) + { + return null; + } + + return targetFrameworks + .OrderBy(f => f.version) + .Select(f => f.tfm) + .FirstOrDefault(); } } @@ -116,5 +130,20 @@ private class MsBuildProperties public string? TargetFrameworks { get; set; } } - + /// + /// Represents the output from dotnet msbuild -getProperty command for framework identifiers. + /// + private class MsBuildFrameworkOutput + { + public MsBuildFrameworkProperties? Properties { get; set; } + } + + /// + /// Represents the framework properties returned from dotnet msbuild -getProperty command. + /// + private class MsBuildFrameworkProperties + { + public string? TargetFrameworkIdentifier { get; set; } + public string? TargetFrameworkVersion { get; set; } + } } From 3c914120776956246f4c72902ae88db41d59c9ea Mon Sep 17 00:00:00 2001 From: haileymck Date: Tue, 16 Dec 2025 16:23:08 -0800 Subject: [PATCH 7/8] verify the target framework version and target framework identifier for all --- .../NuGet/Package.cs | 12 +- .../NuGet/TargetFrameworkConstants.cs | 1 + .../Steps/AddPackagesStep.cs | 2 +- .../AspNet/Common/ProjectInfo.cs | 114 +----------- .../AspNet/Common/TargetFrameworkHelpers.cs | 172 ++++++++++++++++++ .../ScaffoldSteps/ValidateBlazorCrudStep.cs | 2 +- .../ScaffoldSteps/ValidateEfControllerStep.cs | 2 +- .../ScaffoldSteps/ValidateEntraIdStep.cs | 2 +- .../ScaffoldSteps/ValidateIdentityStep.cs | 2 +- .../ScaffoldSteps/ValidateMinimalApiStep.cs | 2 +- .../ScaffoldSteps/ValidateRazorPagesStep.cs | 2 +- .../AspNet/ScaffoldSteps/ValidateViewsStep.cs | 2 +- .../Flow/Steps/ParameterBasedFlowStep.cs | 10 +- 13 files changed, 196 insertions(+), 129 deletions(-) create mode 100644 src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/TargetFrameworkHelpers.cs diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs index 2dacd1eca..54c0cd96f 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs @@ -32,7 +32,7 @@ internal static class PackageExtensions /// The logger to use for logging messages. /// A package instance with the version property set to the resolved version for the specified target framework, or /// the original package if the version is already set or cannot be resolved. - public static async Task WithResolvedVersionAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) + public static async Task WithResolvedVersionAsync(this Package package, string? targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) { if (package.PackageVersion is not null) { @@ -59,13 +59,19 @@ public static async Task WithResolvedVersionAsync(this Package package, /// The logger to use for logging messages. /// A task that represents the asynchronous operation. The task result contains the corresponding NuGet package /// version if available; otherwise, if the package does not require a version or target framework is not supported. - private static Task GetVersionForTargetFrameworkAsync(this Package package, string targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) + private static Task GetVersionForTargetFrameworkAsync(this Package package, string? targetFramework, NuGetVersionService nugetVersionHelper, ILogger? logger = null) { if (!package.IsVersionRequired) { return Task.FromResult(null); } + if (targetFramework is null) + { + logger?.LogError("Project contains a Target Framework that is not supported. Supported Target Frameworks are .NET8, .NET9, .NET10. Installing latest stable version of '{PackageName}'. Consider upgrading your Target Framework to install a compatible package version.", package.Name); + return Task.FromResult(null); + } + if (targetFramework.Equals(TargetFrameworkConstants.Net8, StringComparison.OrdinalIgnoreCase)) { return nugetVersionHelper.GetLatestPackageForNetVersionAsync(package.Name, 8); @@ -80,7 +86,7 @@ public static async Task WithResolvedVersionAsync(this Package package, } else { - logger?.LogError("Target framework '{TargetFramework}' is not supported. Installing latest stable version of '{PackageName}'. Consider upgrading your target framework to install a compatible package version.", targetFramework, package.Name); + logger?.LogError("Target Framework '{TargetFramework}' is not supported. Supported Target Frameworks are .NET8, .NET9, .NET10. Installing latest stable version of '{PackageName}'. Consider upgrading your Target Framework to install a compatible package version.", targetFramework, package.Name); return Task.FromResult(null); } } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs index a9de084da..b54d0c7c0 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/TargetFrameworkConstants.cs @@ -10,4 +10,5 @@ internal static class TargetFrameworkConstants public const string Net8 = "net8.0"; public const string Net9 = "net9.0"; public const string Net10 = "net10.0"; + public const string NetCoreApp = ".NETCoreApp"; } diff --git a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs index 028c3dbea..1a0f9a61e 100644 --- a/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs +++ b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs @@ -56,7 +56,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell { string? packageVersion = null; Package resolvedPackage = package; - if (package.IsVersionRequired && !string.IsNullOrEmpty(targetFramework) && !Prerelease) + if (package.IsVersionRequired && !Prerelease) { resolvedPackage = await package.WithResolvedVersionAsync(targetFramework, _nugetVersionHelper, _logger); packageVersion = resolvedPackage.PackageVersion; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs index c8bc44fef..e2f8156ec 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -1,7 +1,5 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.Json; -using Microsoft.DotNet.Scaffolding.Internal.CliHelpers; using Microsoft.DotNet.Scaffolding.Roslyn.Services; namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Common; @@ -14,7 +12,7 @@ internal class ProjectInfo public ProjectInfo(string? projectPath) { ProjectPath = projectPath; - LowestTargetFramework = projectPath is not null ? GetLowestTargetFrameworkFromCli(projectPath) : null; + LowestSupportedTargetFramework = projectPath is not null ? TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath) : null; } /// @@ -30,120 +28,14 @@ public ProjectInfo(string? projectPath) /// public IList? CodeChangeOptions { get; set; } /// - /// Gets or sets the lowest target framework for the project (if multiple are found). + /// Null if the project contains an unsupported target framework; otherwise, the supported target framework moniker (TFM). /// - public string? LowestTargetFramework { get; } + public string? LowestSupportedTargetFramework { get; } /// /// Gets or sets the list of project capabilities. /// public IList? Capabilities { get; set; } - /// - /// Gets the lowest target framework from the project using dotnet msbuild CLI command. - /// - /// The path to the project file. - /// The lowest target framework moniker (TFM) as a string, or null if not found. - internal static string? GetLowestTargetFrameworkFromCli(string projectPath) - { - try - { - MsBuildPropertiesOutput? msbuildOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize(["-getProperty:TargetFramework;TargetFrameworks"], projectPath); - if (msbuildOutput?.Properties is null) - { - return null; - } - - // If a single TargetFramework is set, return it - if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFramework)) - { - return msbuildOutput.Properties.TargetFramework; - } - - // If multiple TargetFrameworks are set, find the lowest version - if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFrameworks)) - { - string[] frameworks = msbuildOutput.Properties.TargetFrameworks.Split(';'); - string? lowestFramework = GetLowestFromFrameworks(frameworks, projectPath); - - return lowestFramework; - } - - return null; - } - catch (JsonException) - { - return null; - } - - static string? GetLowestFromFrameworks(string[] frameworks, string projectPath) - { - List<(string tfm, Version version)> targetFrameworks = []; - foreach (string tfm in frameworks) - { - try - { - MsBuildFrameworkOutput? frameworkOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize([$"-p:TargetFramework=\"{tfm}\"", - "-getProperty:TargetFrameworkIdentifier;TargetFrameworkVersion"], - projectPath); - - if (frameworkOutput?.Properties?.TargetFrameworkVersion is not null) - { - string version = frameworkOutput.Properties.TargetFrameworkVersion.TrimStart('v'); - if (Version.TryParse(version, out var tfmVersion)) - { - targetFrameworks.Add((tfm, tfmVersion)); - } - } - } - catch (JsonException) - { - continue; - } - } - - if (targetFrameworks.Count == 0) - { - return null; - } - - return targetFrameworks - .OrderBy(f => f.version) - .Select(f => f.tfm) - .FirstOrDefault(); - } - } - - /// - /// Represents the output from dotnet msbuild -getProperty command. - /// - private class MsBuildPropertiesOutput - { - public MsBuildProperties? Properties { get; set; } - } - /// - /// Represents the properties returned from dotnet msbuild -getProperty command. - /// - private class MsBuildProperties - { - public string? TargetFramework { get; set; } - public string? TargetFrameworks { get; set; } - } - /// - /// Represents the output from dotnet msbuild -getProperty command for framework identifiers. - /// - private class MsBuildFrameworkOutput - { - public MsBuildFrameworkProperties? Properties { get; set; } - } - - /// - /// Represents the framework properties returned from dotnet msbuild -getProperty command. - /// - private class MsBuildFrameworkProperties - { - public string? TargetFrameworkIdentifier { get; set; } - public string? TargetFrameworkVersion { get; set; } - } } diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/TargetFrameworkHelpers.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/TargetFrameworkHelpers.cs new file mode 100644 index 000000000..07bbac23b --- /dev/null +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/TargetFrameworkHelpers.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Scaffolding.Core.Model; +using Microsoft.DotNet.Scaffolding.Internal.CliHelpers; + +namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Common; + +internal class TargetFrameworkHelpers +{ + /// + /// Determines the lowest compatible target framework for the specified project file. Returns null if there are any incompatible target frameworks. + /// + /// If the project specifies multiple target frameworks, the method evaluates each and returns + /// the lowest version that is compatible. If none are compatible, or if the project does not specify any target + /// frameworks, the method returns null. + /// The full path to the project file to evaluate. Cannot be null or empty. + /// The target framework moniker (TFM) string representing the lowest compatible framework, or null if no compatible + /// framework is found. + internal static string? GetLowestCompatibleTargetFramework(string projectPath) + { + MsBuildPropertiesOutput? msbuildOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize(["-getProperty:TargetFramework;TargetFrameworks"], projectPath); + if (msbuildOutput?.Properties is null) + { + return null; + } + + // If a single TargetFramework is set, validate its compatiblity and return it + if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFramework)) + { + string tfm = msbuildOutput.Properties.TargetFramework; + if (IsCompatibleFramework(tfm, projectPath, out _)) + { + return tfm; + } + return null; + } + + // If multiple TargetFrameworks are set, find the lowest compatible version, if there isn't any incompatible ones, return null + if (!string.IsNullOrEmpty(msbuildOutput.Properties.TargetFrameworks)) + { + string[] frameworks = msbuildOutput.Properties.TargetFrameworks.Split(';'); + List<(string tfm, Version version)>? compatibleFrameworks = GetCompatibleFrameworks(frameworks, projectPath); + + if (compatibleFrameworks is not null) + { + return GetLowestCompatibleFramework(compatibleFrameworks); + } + } + + return null; + } + + /// + /// Determines which target frameworks from the specified list are compatible with the given project and returns + /// their identifiers and versions. If any framework is found to be incompatible, the method returns null. + /// + /// An array of target framework monikers (TFMs) to check for compatibility with the project. Each element should be + /// a valid TFM string. + /// The file path to the project whose compatibility with the specified frameworks is to be evaluated. Must not be + /// null or empty. + /// A list of tuples containing the TFM and its corresponding version for each compatible framework. Returns null if + /// any framework is incompatible or if no compatible frameworks are found. + private static List<(string tfm, Version version)>? GetCompatibleFrameworks(string[] frameworks, string projectPath) + { + List<(string tfm, Version version)> targetFrameworks = []; + foreach (string tfm in frameworks) + { + if (IsCompatibleFramework(tfm, projectPath, out Version? frameworkVersion)) + { + if (frameworkVersion is not null) + { + targetFrameworks.Add((tfm, frameworkVersion)); + } + } + else + { + // If any framework is incompatible, return null + return null; + } + } + + if (targetFrameworks.Count == 0) + { + return null; + } + + return targetFrameworks; + } + + /// + /// Determines whether the specified target framework moniker (TFM) represents a .NET Core application with a + /// version of 8.0 or higher. + /// + /// This method checks the project's target framework by invoking MSBuild and analyzing the + /// framework identifier and version. Only .NET Core applications with a version of 8.0 or higher are considered + /// compatible. + /// The target framework moniker (TFM) to evaluate, such as "net8.0". + /// The full path to the project file to use when evaluating the framework. + /// Outputs the version of the framework if compatible; otherwise, null. + /// true if the TFM corresponds to .NET Core (netcoreapp) version 8.0 or higher; otherwise, false. + private static bool IsCompatibleFramework(string tfm, string projectPath, out Version? frameworkVersion) + { + frameworkVersion = null; + MsBuildFrameworkOutput? frameworkOutput = MsBuildCliRunner.RunMSBuildCommandAndDeserialize([$"-p:TargetFramework=\"{tfm}\"", + "-getProperty:TargetFrameworkIdentifier;TargetFrameworkVersion"], + projectPath); + + if (frameworkOutput?.Properties?.TargetFrameworkIdentifier is not null && + string.Equals(frameworkOutput.Properties.TargetFrameworkIdentifier, TargetFrameworkConstants.NetCoreApp, StringComparison.OrdinalIgnoreCase)) + { + if (frameworkOutput.Properties.TargetFrameworkVersion is not null) + { + string version = frameworkOutput.Properties.TargetFrameworkVersion.TrimStart('v'); + if (Version.TryParse(version, out Version? tfmVersion)) + { + frameworkVersion = tfmVersion; + return tfmVersion.Major >= 8; + } + } + } + + return false; + } + + /// + /// Determines the target framework moniker (TFM) with the lowest version from the provided list of frameworks. + /// + /// A list of tuples, each containing a target framework moniker (TFM) and its associated version. Cannot be null. + /// The TFM string corresponding to the lowest version in the list, or null if the list is empty. + private static string? GetLowestCompatibleFramework(List<(string tfm, Version version)> frameworks) + { + return frameworks + .OrderBy(f => f.version) + .Select(f => f.tfm) + .FirstOrDefault(); + } + + /// + /// Represents the output from dotnet msbuild -getProperty command. + /// + private class MsBuildPropertiesOutput + { + public MsBuildProperties? Properties { get; set; } + } + + /// + /// Represents the properties returned from dotnet msbuild -getProperty command. + /// + private class MsBuildProperties + { + public string? TargetFramework { get; set; } + public string? TargetFrameworks { get; set; } + } + + /// + /// Represents the output from dotnet msbuild -getProperty command for framework identifiers. + /// + private class MsBuildFrameworkOutput + { + public MsBuildFrameworkProperties? Properties { get; set; } + } + + /// + /// Represents the framework properties returned from dotnet msbuild -getProperty command. + /// + private class MsBuildFrameworkProperties + { + public string? TargetFrameworkIdentifier { get; set; } + public string? TargetFrameworkVersion { get; set; } + } +} diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs index 2bb3954c3..79125897c 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateBlazorCrudStep.cs @@ -203,7 +203,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetBlazorCrudModelAsync(ScaffolderContext context, CrudSettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs index 33f69acd0..1c160eeb9 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEfControllerStep.cs @@ -211,7 +211,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetEfControllerModelAsync(ScaffolderContext context, EfControllerSettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs index fe3fc5261..6b95b14a2 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateEntraIdStep.cs @@ -166,7 +166,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell } ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo?.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs index 7949aeca7..fe5d105c1 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateIdentityStep.cs @@ -187,7 +187,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetIdentityModelAsync(ScaffolderContext context, IdentitySettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs index beb8df14a..36e53ec33 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateMinimalApiStep.cs @@ -194,7 +194,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetMinimalApiModelAsync(ScaffolderContext context, MinimalApiSettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs index 48711d749..ae6d9ba79 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateRazorPagesStep.cs @@ -193,7 +193,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetRazorPageModelAsync(ScaffolderContext context, CrudSettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); var projectDirectory = Path.GetDirectoryName(projectInfo.ProjectPath); if (projectInfo is null || projectInfo.CodeService is null || string.IsNullOrEmpty(projectDirectory)) { diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs index 780153bb7..21b315a63 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/ScaffoldSteps/ValidateViewsStep.cs @@ -134,7 +134,7 @@ public override async Task ExecuteAsync(ScaffolderContext context, Cancell private async Task GetViewModelAsync(ScaffolderContext context, CrudSettings settings) { ProjectInfo projectInfo = ClassAnalyzers.GetProjectInfo(settings.Project, _logger); - context.SetSpecifiedTargetFramework(projectInfo.LowestTargetFramework); + context.SetSpecifiedTargetFramework(projectInfo.LowestSupportedTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs index 84cfde49d..15d618836 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/Interactive/Flow/Steps/ParameterBasedFlowStep.cs @@ -178,7 +178,7 @@ private void SelectCodeService(IFlowContext context, string projectPath) /// skipped. /// The flow context containing project information and properties. Must not be null. /// true if the prerelease option should be skipped for the current project; otherwise, false. - private bool ShouldSkipPrereleaseOption(IFlowContext context) + private static bool ShouldSkipPrereleaseOption(IFlowContext context) { //TODO update with each major release of .NET @@ -187,12 +187,8 @@ private bool ShouldSkipPrereleaseOption(IFlowContext context) if (context.Properties.Get(projectParameterKey) is FlowProperty projectFileProperty && projectFileProperty.Value is string projectFilePath && !string.IsNullOrEmpty(projectFilePath)) { - string? targetFramework = ProjectInfo.GetLowestTargetFrameworkFromCli(projectFilePath); - if (!string.IsNullOrEmpty(targetFramework) && !targetFramework.Equals(TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase)) - { - // skip the prerelease step for non-net10 projects - return true; - } + string? targetFramework = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectFilePath); + return targetFramework is null || !targetFramework.Equals(TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase); } return false; } From b2598848253c83f3a74ba23b1b076a6be88567bd Mon Sep 17 00:00:00 2001 From: haileymck Date: Tue, 16 Dec 2025 16:36:21 -0800 Subject: [PATCH 8/8] add unit tests for target framework parsing --- All.sln | 23 +- .../Properties/AssemblyInfo.cs | 6 + .../Common/TargetFrameworkHelpersTests.cs | 301 ++++++++++++++++++ .../dotnet-scaffold.Tests.csproj | 13 + 4 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 src/dotnet-scaffolding/dotnet-scaffold/Properties/AssemblyInfo.cs create mode 100644 test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Common/TargetFrameworkHelpersTests.cs create mode 100644 test/dotnet-scaffolding/dotnet-scaffold.Tests/dotnet-scaffold.Tests.csproj diff --git a/All.sln b/All.sln index ef695caf4..6330d533b 100644 --- a/All.sln +++ b/All.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11022.115 -MinimumVisualStudioVersion = 15.0.26730.03 +MinimumVisualStudioVersion = 15.0.26730.3 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A4057ACF-27F0-4724-963B-44548B6BC4E9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{81B64EBF-613D-43AE-82DA-B375FB751921}" @@ -129,6 +129,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-scaffolding", "dotne EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.Scaffolding.Roslyn.Tests", "test\dotnet-scaffolding\Microsoft.DotNet.Scaffolding.Roslyn.Tests\Microsoft.DotNet.Scaffolding.Roslyn.Tests.csproj", "{83AFCAC3-4AED-49A4-A7C1-61EC051562E4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-scaffold.Tests", "test\dotnet-scaffolding\dotnet-scaffold.Tests\dotnet-scaffold.Tests.csproj", "{992AB1BF-23A3-40DB-A3A5-06C80C760973}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution debug_x86|Any CPU = debug_x86|Any CPU @@ -808,6 +810,24 @@ Global {83AFCAC3-4AED-49A4-A7C1-61EC051562E4}.Release|x64.Build.0 = Release|Any CPU {83AFCAC3-4AED-49A4-A7C1-61EC051562E4}.Release|x86.ActiveCfg = Release|Any CPU {83AFCAC3-4AED-49A4-A7C1-61EC051562E4}.Release|x86.Build.0 = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|Any CPU.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|Any CPU.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|x64.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|x64.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|x86.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.debug_x86|x86.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|Any CPU.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|x64.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|x64.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|x86.ActiveCfg = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Debug|x86.Build.0 = Debug|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|Any CPU.ActiveCfg = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|Any CPU.Build.0 = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|x64.ActiveCfg = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|x64.Build.0 = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|x86.ActiveCfg = Release|Any CPU + {992AB1BF-23A3-40DB-A3A5-06C80C760973}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -859,6 +879,7 @@ Global {2180D82F-38EE-4FD6-927F-03BEE22B789E} = {73FF6F8B-3D15-41F1-9358-A497DAE4AD8B} {0E27E3F4-C93F-4DF1-8F4D-1E62A6641C75} = {81B64EBF-613D-43AE-82DA-B375FB751921} {83AFCAC3-4AED-49A4-A7C1-61EC051562E4} = {0E27E3F4-C93F-4DF1-8F4D-1E62A6641C75} + {992AB1BF-23A3-40DB-A3A5-06C80C760973} = {0E27E3F4-C93F-4DF1-8F4D-1E62A6641C75} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {26BCDDB3-5505-4903-9D87-C942ED0D03E6} diff --git a/src/dotnet-scaffolding/dotnet-scaffold/Properties/AssemblyInfo.cs b/src/dotnet-scaffolding/dotnet-scaffold/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..7c619c524 --- /dev/null +++ b/src/dotnet-scaffolding/dotnet-scaffold/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dotnet-scaffold.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Common/TargetFrameworkHelpersTests.cs b/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Common/TargetFrameworkHelpersTests.cs new file mode 100644 index 000000000..f37eeef59 --- /dev/null +++ b/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Common/TargetFrameworkHelpersTests.cs @@ -0,0 +1,301 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.DotNet.Scaffolding.Internal.CliHelpers; +using Microsoft.DotNet.Tools.Scaffold.AspNet.Common; +using Xunit; + +namespace Microsoft.DotNet.Tools.Scaffold.Tests.AspNet.Common; + +public class TargetFrameworkHelpersTests : IDisposable +{ + private readonly string _testProjectsDirectory; + private readonly List _createdProjects; + + public TargetFrameworkHelpersTests() + { + _testProjectsDirectory = Path.Combine(Path.GetTempPath(), "TargetFrameworkHelpersTests", Guid.NewGuid().ToString()); + Directory.CreateDirectory(_testProjectsDirectory); + _createdProjects = new List(); + } + + public void Dispose() + { + // Cleanup test projects + if (Directory.Exists(_testProjectsDirectory)) + { + try + { + Directory.Delete(_testProjectsDirectory, recursive: true); + } + catch + { + // Ignore cleanup errors + } + } + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net8Project_ReturnsNet8() + { + // Arrange + string projectPath = CreateTestProject("TestNet8.csproj", "net8.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net8.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net9Project_ReturnsNet9() + { + // Arrange + string projectPath = CreateTestProject("TestNet9.csproj", "net9.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net9.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net10Project_ReturnsNet10() + { + // Arrange + string projectPath = CreateTestProject("TestNet10.csproj", "net10.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net10.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetNet8AndNet9_ReturnsNet8() + { + // Arrange + string projectPath = CreateTestProject("TestMultiTarget.csproj", "net8.0;net9.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net8.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetNet9AndNet10_ReturnsNet9() + { + // Arrange + string projectPath = CreateTestProject("TestMultiTarget2.csproj", "net9.0;net10.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net9.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetNet8Net9Net10_ReturnsNet8() + { + // Arrange + string projectPath = CreateTestProject("TestMultiTarget3.csproj", "net8.0;net9.0;net10.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net8.0", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net7Project_ReturnsNull() + { + // Arrange + string projectPath = CreateTestProject("TestNet7.csproj", "net7.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net6Project_ReturnsNull() + { + // Arrange + string projectPath = CreateTestProject("TestNet6.csproj", "net6.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_NetStandard20Project_ReturnsNull() + { + // Arrange + string projectPath = CreateTestProject("TestNetStandard.csproj", "netstandard2.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MonoAndroidProject_ReturnsNull() + { + // Arrange + string projectPath = CreateTestProject("TestMonoAndroid.csproj", "monoandroid13.0"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetWithIncompatible_ReturnsNull() + { + // Arrange - Mix of compatible (net8.0) and incompatible (net7.0) frameworks + string projectPath = CreateTestProject("TestMixedTarget.csproj", "net7.0;net8.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); // Should return null because net7.0 is incompatible + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetWithMonoAndroid_ReturnsNull() + { + // Arrange - Mix of compatible (net9.0) and incompatible (monoandroid13.0) frameworks + string projectPath = CreateTestProject("TestMixedTarget2.csproj", "net9.0;monoandroid13.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); // Should return null because monoandroid13.0 is incompatible + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net8Android_ReturnsNet8Android() + { + // Arrange + string projectPath = CreateTestProject("TestNet8Android.csproj", "net8.0-android"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net8.0-android", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_Net9iOS_ReturnsNet9iOS() + { + // Arrange + string projectPath = CreateTestProject("TestNet9iOS.csproj", "net9.0-ios"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net9.0-ios", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_MultiTargetNet8AndroidAndNet9_ReturnsNet8Android() + { + // Arrange + string projectPath = CreateTestProject("TestMultiPlatform.csproj", "net8.0-android;net9.0", isMultiTarget: true); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.NotNull(result); + Assert.Equal("net8.0-android", result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_InvalidProjectPath_ReturnsNull() + { + // Arrange + string projectPath = Path.Combine(_testProjectsDirectory, "NonExistent.csproj"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetLowestCompatibleTargetFramework_EmptyProject_ReturnsNull() + { + // Arrange + string projectPath = CreateEmptyProject("EmptyProject.csproj"); + + // Act + string? result = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectPath); + + // Assert + Assert.Null(result); + } + + private string CreateTestProject(string projectName, string targetFramework, bool isMultiTarget = false) + { + string projectPath = Path.Combine(_testProjectsDirectory, projectName); + string frameworkProperty = isMultiTarget ? "TargetFrameworks" : "TargetFramework"; + + string projectContent = $@" + + <{frameworkProperty}>{targetFramework} + Exe + +"; + + File.WriteAllText(projectPath, projectContent); + _createdProjects.Add(projectPath); + return projectPath; + } + + private string CreateEmptyProject(string projectName) + { + string projectPath = Path.Combine(_testProjectsDirectory, projectName); + + string projectContent = @" + + +"; + + File.WriteAllText(projectPath, projectContent); + _createdProjects.Add(projectPath); + return projectPath; + } +} diff --git a/test/dotnet-scaffolding/dotnet-scaffold.Tests/dotnet-scaffold.Tests.csproj b/test/dotnet-scaffolding/dotnet-scaffold.Tests/dotnet-scaffold.Tests.csproj new file mode 100644 index 000000000..2561c88bd --- /dev/null +++ b/test/dotnet-scaffolding/dotnet-scaffold.Tests/dotnet-scaffold.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + false + + + + + + + +