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/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/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/Package.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/NuGet/Package.cs index 25ce9d709..54c0cd96f 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,17 +56,22 @@ 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) { 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); @@ -79,7 +86,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. 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 e21a6dd73..b54d0c7c0 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,14 @@ // 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]; + public const string NetCoreApp = ".NETCoreApp"; } 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/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs b/src/dotnet-scaffolding/Microsoft.DotNet.Scaffolding.Core/Steps/AddPackagesStep.cs index 26592fa7a..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,9 +56,9 @@ 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); + resolvedPackage = await package.WithResolvedVersionAsync(targetFramework, _nugetVersionHelper, _logger); packageVersion = resolvedPackage.PackageVersion; } 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/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..e2f8156ec 100644 --- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs +++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Common/ProjectInfo.cs @@ -9,10 +9,16 @@ namespace Microsoft.DotNet.Tools.Scaffold.AspNet.Common; /// internal class ProjectInfo { + public ProjectInfo(string? projectPath) + { + ProjectPath = projectPath; + LowestSupportedTargetFramework = projectPath is not null ? TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(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. /// @@ -22,11 +28,14 @@ internal class ProjectInfo /// 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; set; } + public string? LowestSupportedTargetFramework { get; } /// /// Gets or sets the list of project capabilities. /// public IList? Capabilities { 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 9f44795b9..79125897c 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.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 d8f1ff5b6..1c160eeb9 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.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 c25f5452c..6b95b14a2 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.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 0691fefcb..fe5d105c1 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.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 9de01c372..36e53ec33 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.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 b65fc10ba..ae6d9ba79 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.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 a2bdb9b11..21b315a63 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.LowestSupportedTargetFramework); if (projectInfo is null || projectInfo.CodeService is null) { return null; 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 8b3c15502..15d618836 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,9 @@ public async ValueTask RunAsync(IFlowContext context, Cancellati { NextStep = NextStep?.NextStep; } - else if (ParameterHelpers.IsTargetFrameworkOption(Parameter) && !string.Equals(parameterValue, TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase)) + + if (NextStep is not null && NextStep.Parameter.DisplayName.Equals(AspnetStrings.Options.Prerelease.DisplayName, StringComparison.Ordinal) && ShouldSkipPrereleaseOption(context)) { - // 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; } @@ -167,5 +168,29 @@ 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 static 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 = TargetFrameworkHelpers.GetLowestCompatibleTargetFramework(projectFilePath); + return targetFramework is null || !targetFramework.Equals(TargetFrameworkConstants.Net10, StringComparison.OrdinalIgnoreCase); + } + return false; + } } } 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/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/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 + + + + + + + +