diff --git a/src/dotnet-ef/ProjectOptions.cs b/src/dotnet-ef/ProjectOptions.cs index b2f9222cc6c..a2b719fe6d4 100644 --- a/src/dotnet-ef/ProjectOptions.cs +++ b/src/dotnet-ef/ProjectOptions.cs @@ -9,7 +9,9 @@ namespace Microsoft.EntityFrameworkCore.Tools; internal class ProjectOptions { public CommandOption? Project { get; private set; } + public CommandOption? File { get; private set; } public CommandOption? StartupProject { get; private set; } + public CommandOption? StartupFile { get; private set; } public CommandOption? Framework { get; private set; } public CommandOption? Configuration { get; private set; } public CommandOption? Runtime { get; private set; } @@ -20,7 +22,9 @@ internal class ProjectOptions public void Configure(CommandLineApplication command) { Project = command.Option("-p|--project ", Resources.ProjectDescription); + File = command.Option("--file ", Resources.FileDescription); StartupProject = command.Option("-s|--startup-project ", Resources.StartupProjectDescription); + StartupFile = command.Option("--startup-file ", Resources.StartupFileDescription); Framework = command.Option("--framework ", Resources.FrameworkDescription); Configuration = command.Option("--configuration ", Resources.ConfigurationDescription); Runtime = command.Option("--runtime ", Resources.RuntimeDescription); diff --git a/src/dotnet-ef/Properties/Resources.Designer.cs b/src/dotnet-ef/Properties/Resources.Designer.cs index e8714245063..a7f345cac2b 100644 --- a/src/dotnet-ef/Properties/Resources.Designer.cs +++ b/src/dotnet-ef/Properties/Resources.Designer.cs @@ -223,6 +223,12 @@ public static string DotNetEfConfigUnknownProperty(object? configFile, object? p public static string EFFullName => GetString("EFFullName"); + /// + /// The file-based app to use. An alias for --project. + /// + public static string FileDescription + => GetString("FileDescription"); + /// /// The target framework. Defaults to the first one in the project. /// @@ -385,6 +391,14 @@ public static string MultipleProjectsInDirectory(object? projectDir) public static string MultipleStartupProjects => GetString("MultipleStartupProjects"); + /// + /// The --{option1} and --{option2} options cannot be used together. + /// + public static string MutuallyExclusiveOptions(object? option1, object? option2) + => string.Format( + GetString("MutuallyExclusiveOptions", nameof(option1), nameof(option2)), + option1, option2); + /// /// The project targets multiple frameworks. Use the --framework option to specify which target framework to use. /// @@ -522,7 +536,7 @@ public static string PrefixDescription => GetString("PrefixDescription"); /// - /// The project or file-based app to use. Defaults to the current working directory. + /// The project to use. Defaults to the current working directory. /// public static string ProjectDescription => GetString("ProjectDescription"); @@ -566,11 +580,17 @@ public static string SelfContainedDescription => GetString("SelfContainedDescription"); /// - /// The startup project or file-based app to use. Defaults to the current working directory. + /// The startup project to use. Defaults to the current working directory. /// public static string StartupProjectDescription => GetString("StartupProjectDescription"); + /// + /// The startup file-based app to use. An alias for --startup-project. + /// + public static string StartupFileDescription + => GetString("StartupFileDescription"); + /// /// The suffix to attach to the name of all the generated files /// diff --git a/src/dotnet-ef/Properties/Resources.resx b/src/dotnet-ef/Properties/Resources.resx index 9a68c50fee2..ea1e3c9fb0e 100644 --- a/src/dotnet-ef/Properties/Resources.resx +++ b/src/dotnet-ef/Properties/Resources.resx @@ -192,6 +192,9 @@ Entity Framework Core .NET Command-line Tools + + The file-based app to use. An alias for --project. + Unable to read '{configFile}'. Fix the JSON and try again. {details} @@ -291,6 +294,9 @@ More than one project was found in the current working directory. Use the --startup-project option. + + The --{option1} and --{option2} options cannot be used together. + The project targets multiple frameworks. Use the --framework option to specify which target framework to use. @@ -355,7 +361,7 @@ Prefix output with level. - The project or file-based app to use. Defaults to the current working directory. + The project to use. Defaults to the current working directory. Obsolete @@ -376,7 +382,10 @@ Also bundle the .NET runtime so it doesn't need to be installed on the machine. - The startup project or file-based app to use. Defaults to the current working directory. + The startup project to use. Defaults to the current working directory. + + + The startup file-based app to use. An alias for --startup-project. The suffix to attach to the name of all the generated files diff --git a/src/dotnet-ef/RootCommand.cs b/src/dotnet-ef/RootCommand.cs index 4e3180b3b40..2ad503fab6e 100644 --- a/src/dotnet-ef/RootCommand.cs +++ b/src/dotnet-ef/RootCommand.cs @@ -15,7 +15,9 @@ internal class RootCommand : CommandBase { private CommandLineApplication? _command; private CommandOption? _project; + private CommandOption? _file; private CommandOption? _startupProject; + private CommandOption? _startupFile; private CommandOption? _framework; private CommandOption? _configuration; private CommandOption? _runtime; @@ -33,7 +35,9 @@ public override void Configure(CommandLineApplication command) options.Configure(command); _project = options.Project; + _file = options.File; _startupProject = options.StartupProject; + _startupFile = options.StartupFile; _framework = options.Framework; _configuration = options.Configuration; _runtime = options.Runtime; @@ -60,8 +64,8 @@ protected override int Execute(string[] _) } var config = DotNetEfConfigLoader.Load(Directory.GetCurrentDirectory()); - var projectPath = _project!.Value() ?? config?.Project; - var startupProjectPath = _startupProject!.Value() ?? config?.StartupProject; + var projectPath = ResolveOption(_project!, _file!, config?.Project); + var startupProjectPath = ResolveOption(_startupProject!, _startupFile!, config?.StartupProject); var framework = _framework!.Value() ?? config?.Framework; var configuration = _configuration!.Value() ?? config?.Configuration; var runtime = _runtime!.Value() ?? config?.Runtime; @@ -304,6 +308,20 @@ private static (string, string) ResolveProjects( return (projects[0], startupProjects[0]); } + internal static string? ResolveOption( + CommandOption primary, + CommandOption alias, + string? configValue) + { + if (primary.HasValue() && alias.HasValue()) + { + throw new CommandException( + Resources.MutuallyExclusiveOptions(primary.LongName!, alias.LongName!)); + } + + return alias.Value() ?? primary.Value() ?? configValue; + } + private static List ResolveProjects(string? path) { if (path == null) diff --git a/test/dotnet-ef.Tests/ProjectTest.cs b/test/dotnet-ef.Tests/ProjectTest.cs index 87685e56767..49ceffb9cd7 100644 --- a/test/dotnet-ef.Tests/ProjectTest.cs +++ b/test/dotnet-ef.Tests/ProjectTest.cs @@ -1,12 +1,74 @@ // 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.Cli.CommandLine; + namespace Microsoft.EntityFrameworkCore.Tools; public sealed class ProjectTest(ITestOutputHelper output) { private const string TargetFramework = "net10.0"; + [Fact] + public void Alias_option_is_used() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + alias.TryParse("MyApp.cs"); + + Assert.Equal("MyApp.cs", RootCommand.ResolveOption(primary, alias, configValue: null)); + } + + [Fact] + public void Primary_option_is_used_when_alias_is_not_specified() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + primary.TryParse("MyApp.csproj"); + + Assert.Equal("MyApp.csproj", RootCommand.ResolveOption(primary, alias, configValue: null)); + } + + [Fact] + public void Config_value_is_used_when_no_options_specified() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + + Assert.Equal("FromConfig", RootCommand.ResolveOption(primary, alias, configValue: "FromConfig")); + } + + [Fact] + public void Alias_option_takes_precedence_over_config() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + alias.TryParse("MyApp.cs"); + + Assert.Equal("MyApp.cs", RootCommand.ResolveOption(primary, alias, configValue: "FromConfig")); + } + + [Fact] + public void Primary_and_alias_options_together_throws() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + primary.TryParse("MyApp.csproj"); + alias.TryParse("MyApp.cs"); + + Assert.Throws( + () => RootCommand.ResolveOption(primary, alias, configValue: null)); + } + + [Fact] + public void Returns_null_when_nothing_specified() + { + var primary = CreateOption("--project"); + var alias = CreateOption("--file"); + + Assert.Null(RootCommand.ResolveOption(primary, alias, configValue: null)); + } + [Fact] public void Csproj_metadata_can_be_extracted() { @@ -92,4 +154,7 @@ public override void WriteLine(string? value) } } } + + private static CommandOption CreateOption(string name) + => new($"{name} ", CommandOptionType.SingleValue); }