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}{frameworkProperty}>
+ 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
+
+
+
+
+
+
+
+