From 013cb8f66b29eec053f3b7bdf10825af963f990d Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Mon, 15 May 2023 05:29:46 +0300 Subject: [PATCH] Add an overload of `UseTypeActivator(...)` that takes a list of added command types --- CliFx.Demo/Program.cs | 28 +++---- CliFx.Tests/TypeActivationSpecs.cs | 19 +++-- CliFx/CliApplicationBuilder.cs | 82 +++++++++++-------- CliFx/Infrastructure/DelegateTypeActivator.cs | 10 +-- CliFx/Utils/PathEx.cs | 22 +++++ Readme.md | 30 +++---- 6 files changed, 117 insertions(+), 74 deletions(-) create mode 100644 CliFx/Utils/PathEx.cs diff --git a/CliFx.Demo/Program.cs b/CliFx.Demo/Program.cs index 3a05f47..3494d74 100644 --- a/CliFx.Demo/Program.cs +++ b/CliFx.Demo/Program.cs @@ -1,25 +1,21 @@ using CliFx; -using CliFx.Demo.Commands; using CliFx.Demo.Domain; using Microsoft.Extensions.DependencyInjection; -// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands -var services = new ServiceCollection(); - -// Register services -services.AddSingleton(); - -// Register commands -services.AddTransient(); -services.AddTransient(); -services.AddTransient(); -services.AddTransient(); - -var serviceProvider = services.BuildServiceProvider(); - return await new CliApplicationBuilder() .SetDescription("Demo application showcasing CliFx features.") .AddCommandsFromThisAssembly() - .UseTypeActivator(serviceProvider.GetRequiredService) + .UseTypeActivator(commandTypes => + { + // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands + var services = new ServiceCollection(); + services.AddSingleton(); + + // Register all commands as transient services + foreach (var commandType in commandTypes) + services.AddTransient(commandType); + + return services.BuildServiceProvider(); + }) .Build() .RunAsync(); \ No newline at end of file diff --git a/CliFx.Tests/TypeActivationSpecs.cs b/CliFx.Tests/TypeActivationSpecs.cs index 09eb523..d84d5c0 100644 --- a/CliFx.Tests/TypeActivationSpecs.cs +++ b/CliFx.Tests/TypeActivationSpecs.cs @@ -156,14 +156,23 @@ public class TypeActivationSpecs : SpecsBase """ ); - var serviceProvider = new ServiceCollection() - .AddSingleton(commandType, Activator.CreateInstance(commandType, "Hello world")!) - .BuildServiceProvider(); - var application = new CliApplicationBuilder() .AddCommand(commandType) .UseConsole(FakeConsole) - .UseTypeActivator(serviceProvider) + .UseTypeActivator(commandTypes => + { + var services = new ServiceCollection(); + + foreach (var serviceType in commandTypes) + { + services.AddSingleton( + serviceType, + Activator.CreateInstance(serviceType, "Hello world")! + ); + } + + return services.BuildServiceProvider(); + }) .Build(); // Act diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index e88a4dc..51a7676 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -33,7 +33,6 @@ public partial class CliApplicationBuilder public CliApplicationBuilder AddCommand(Type commandType) { _commandTypes.Add(commandType); - return this; } @@ -112,7 +111,7 @@ public partial class CliApplicationBuilder } /// - /// Sets application title, which is shown in the help text. + /// Sets the application title, which is shown in the help text. /// /// /// By default, application title is inferred from the assembly name. @@ -124,11 +123,10 @@ public partial class CliApplicationBuilder } /// - /// Sets application executable name, which is shown in the help text. + /// Sets the application executable name, which is shown in the help text. /// /// /// By default, application executable name is inferred from the assembly file name. - /// The file name is also prefixed with `dotnet` if it's a DLL file. /// public CliApplicationBuilder SetExecutableName(string executableName) { @@ -137,8 +135,7 @@ public partial class CliApplicationBuilder } /// - /// Sets application version, which is shown in the help text or - /// when the user specifies the version option. + /// Sets the application version, which is shown in the help text or when the user specifies the version option. /// /// /// By default, application version is inferred from the assembly version. @@ -150,7 +147,7 @@ public partial class CliApplicationBuilder } /// - /// Sets application description, which is shown in the help text. + /// Sets the application description, which is shown in the help text. /// public CliApplicationBuilder SetDescription(string? description) { @@ -177,10 +174,10 @@ public partial class CliApplicationBuilder } /// - /// Configures the application to use the specified function for activating types. + /// Configures the application to use the specified delegate for activating types. /// - public CliApplicationBuilder UseTypeActivator(Func typeActivator) => - UseTypeActivator(new DelegateTypeActivator(typeActivator)); + public CliApplicationBuilder UseTypeActivator(Func createInstance) => + UseTypeActivator(new DelegateTypeActivator(createInstance)); /// /// Configures the application to use the specified service provider for activating types. @@ -188,6 +185,14 @@ public partial class CliApplicationBuilder public CliApplicationBuilder UseTypeActivator(IServiceProvider serviceProvider) => UseTypeActivator(serviceProvider.GetService); + /// + /// Configures the application to use the specified service provider for activating types. + /// This method takes a delegate that receives the list of all added command types, so that you can + /// easily register them with the service provider. + /// + public CliApplicationBuilder UseTypeActivator(Func, IServiceProvider> getServiceProvider) => + UseTypeActivator(getServiceProvider(_commandTypes.ToArray())); + /// /// Creates a configured instance of . /// @@ -221,45 +226,56 @@ public partial class CliApplicationBuilder { var entryAssemblyName = EnvironmentEx.EntryAssembly?.GetName().Name; if (string.IsNullOrWhiteSpace(entryAssemblyName)) - return "App"; + { + throw new InvalidOperationException( + "Failed to infer the default application title. " + + $"Please specify it explicitly using {nameof(SetTitle)}()." + ); + } return entryAssemblyName; } private static string GetDefaultExecutableName() { - var entryAssemblyLocation = EnvironmentEx.EntryAssembly?.Location; - if (string.IsNullOrWhiteSpace(entryAssemblyLocation)) - return "app"; + var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location; + var processFilePath = EnvironmentEx.ProcessPath; - // If the application was launched via matching EXE apphost, use that as the executable name - var isLaunchedViaAppHost = string.Equals( - EnvironmentEx.ProcessPath, - Path.ChangeExtension(entryAssemblyLocation, ".exe"), - StringComparison.OrdinalIgnoreCase - ); + if (string.IsNullOrWhiteSpace(entryAssemblyFilePath) || string.IsNullOrWhiteSpace(processFilePath)) + { + throw new InvalidOperationException( + "Failed to infer the default application executable name. " + + $"Please specify it explicitly using {nameof(SetExecutableName)}()." + ); + } - if (isLaunchedViaAppHost) - return Path.GetFileNameWithoutExtension(entryAssemblyLocation); + // If the process path matches the entry assembly path, it's a legacy .NET Framework app + // or a self-contained .NET Core app. + if (PathEx.AreEqual(entryAssemblyFilePath, processFilePath)) + return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); - // Otherwise, use the entry assembly as the executable name. - // Prefix it with `dotnet` if it's a DLL file. - var isDll = string.Equals( - Path.GetExtension(entryAssemblyLocation), - ".dll", - StringComparison.OrdinalIgnoreCase - ); + // If the process path has the same name and parent directory as the entry assembly path, + // but different extension, it's a framework-dependent .NET Core app launched through the apphost. + if (PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, ".exe"), processFilePath) || + PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, ""), processFilePath)) + { + return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); + } - return isDll - ? "dotnet " + Path.GetFileName(entryAssemblyLocation) - : Path.GetFileNameWithoutExtension(entryAssemblyLocation); + // Otherwise, it's a framework-dependent .NET Core app launched through the .NET CLI + return "dotnet " + Path.GetFileName(entryAssemblyFilePath); } private static string GetDefaultVersionText() { var entryAssemblyVersion = EnvironmentEx.EntryAssembly?.GetName().Version; if (entryAssemblyVersion is null) - return "v1.0"; + { + throw new InvalidOperationException( + "Failed to infer the default application version. " + + $"Please specify it explicitly using {nameof(SetVersion)}()." + ); + } return "v" + entryAssemblyVersion.ToSemanticString(); } diff --git a/CliFx/Infrastructure/DelegateTypeActivator.cs b/CliFx/Infrastructure/DelegateTypeActivator.cs index 5fd8011..e5d4e67 100644 --- a/CliFx/Infrastructure/DelegateTypeActivator.cs +++ b/CliFx/Infrastructure/DelegateTypeActivator.cs @@ -4,22 +4,22 @@ using CliFx.Exceptions; namespace CliFx.Infrastructure; /// -/// Implementation of that instantiates an object -/// by using a predefined function. +/// Implementation of that instantiates an object by using a predefined delegate. /// public class DelegateTypeActivator : ITypeActivator { - private readonly Func _func; + private readonly Func _createInstance; /// /// Initializes an instance of . /// - public DelegateTypeActivator(Func func) => _func = func; + public DelegateTypeActivator(Func createInstance) => + _createInstance = createInstance; /// public object CreateInstance(Type type) { - var instance = _func(type); + var instance = _createInstance(type); if (instance is null) { diff --git a/CliFx/Utils/PathEx.cs b/CliFx/Utils/PathEx.cs new file mode 100644 index 0000000..8ba04b1 --- /dev/null +++ b/CliFx/Utils/PathEx.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace CliFx.Utils; + +internal static class PathEx +{ + private static StringComparer EqualityComparer { get; } = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; + + public static bool AreEqual(string path1, string path2) + { + static string Normalize(string path) => Path + .GetFullPath(path) + .Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + + return EqualityComparer.Equals(Normalize(path1), Normalize(path2)); + } +} \ No newline at end of file diff --git a/Readme.md b/Readme.md index 6134011..f368e34 100644 --- a/Readme.md +++ b/Readme.md @@ -531,24 +531,24 @@ The following example shows how to configure your application to use [`Microsoft ```csharp public static class Program { - public static async Task Main() - { - var services = new ServiceCollection(); - - // Register services - services.AddSingleton(); - - // Register commands - services.AddTransient(); - - var serviceProvider = services.BuildServiceProvider(); - - return await new CliApplicationBuilder() + public static async Task Main() => + await new CliApplicationBuilder() .AddCommandsFromThisAssembly() - .UseTypeActivator(serviceProvider) + .UseTypeActivator(commandTypes => + { + var services = new ServiceCollection(); + + // Register services + services.AddSingleton(); + + // Register commands + foreach (var commandType in commandTypes) + services.AddTransient(commandType); + + return services.BuildServiceProvider(); + }) .Build() .RunAsync(); - } } ```