Add an overload of UseTypeActivator(...) that takes a list of added command types

This commit is contained in:
Tyrrrz
2023-05-15 05:29:46 +03:00
parent 9c715f458e
commit 013cb8f66b
6 changed files with 117 additions and 74 deletions

View File

@@ -1,25 +1,21 @@
using CliFx; using CliFx;
using CliFx.Demo.Commands;
using CliFx.Demo.Domain; using CliFx.Demo.Domain;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
// Register services
services.AddSingleton<LibraryProvider>();
// Register commands
services.AddTransient<BookCommand>();
services.AddTransient<BookAddCommand>();
services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider();
return await new CliApplicationBuilder() return await new CliApplicationBuilder()
.SetDescription("Demo application showcasing CliFx features.") .SetDescription("Demo application showcasing CliFx features.")
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseTypeActivator(serviceProvider.GetRequiredService) .UseTypeActivator(commandTypes =>
{
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection();
services.AddSingleton<LibraryProvider>();
// Register all commands as transient services
foreach (var commandType in commandTypes)
services.AddTransient(commandType);
return services.BuildServiceProvider();
})
.Build() .Build()
.RunAsync(); .RunAsync();

View File

@@ -156,14 +156,23 @@ public class TypeActivationSpecs : SpecsBase
""" """
); );
var serviceProvider = new ServiceCollection()
.AddSingleton(commandType, Activator.CreateInstance(commandType, "Hello world")!)
.BuildServiceProvider();
var application = new CliApplicationBuilder() var application = new CliApplicationBuilder()
.AddCommand(commandType) .AddCommand(commandType)
.UseConsole(FakeConsole) .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(); .Build();
// Act // Act

View File

@@ -33,7 +33,6 @@ public partial class CliApplicationBuilder
public CliApplicationBuilder AddCommand(Type commandType) public CliApplicationBuilder AddCommand(Type commandType)
{ {
_commandTypes.Add(commandType); _commandTypes.Add(commandType);
return this; return this;
} }
@@ -112,7 +111,7 @@ public partial class CliApplicationBuilder
} }
/// <summary> /// <summary>
/// Sets application title, which is shown in the help text. /// Sets the application title, which is shown in the help text.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// By default, application title is inferred from the assembly name. /// By default, application title is inferred from the assembly name.
@@ -124,11 +123,10 @@ public partial class CliApplicationBuilder
} }
/// <summary> /// <summary>
/// Sets application executable name, which is shown in the help text. /// Sets the application executable name, which is shown in the help text.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// By default, application executable name is inferred from the assembly file name. /// 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.
/// </remarks> /// </remarks>
public CliApplicationBuilder SetExecutableName(string executableName) public CliApplicationBuilder SetExecutableName(string executableName)
{ {
@@ -137,8 +135,7 @@ public partial class CliApplicationBuilder
} }
/// <summary> /// <summary>
/// Sets application version, which is shown in the help text or /// Sets the application version, which is shown in the help text or when the user specifies the version option.
/// when the user specifies the version option.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// By default, application version is inferred from the assembly version. /// By default, application version is inferred from the assembly version.
@@ -150,7 +147,7 @@ public partial class CliApplicationBuilder
} }
/// <summary> /// <summary>
/// Sets application description, which is shown in the help text. /// Sets the application description, which is shown in the help text.
/// </summary> /// </summary>
public CliApplicationBuilder SetDescription(string? description) public CliApplicationBuilder SetDescription(string? description)
{ {
@@ -177,10 +174,10 @@ public partial class CliApplicationBuilder
} }
/// <summary> /// <summary>
/// Configures the application to use the specified function for activating types. /// Configures the application to use the specified delegate for activating types.
/// </summary> /// </summary>
public CliApplicationBuilder UseTypeActivator(Func<Type, object> typeActivator) => public CliApplicationBuilder UseTypeActivator(Func<Type, object> createInstance) =>
UseTypeActivator(new DelegateTypeActivator(typeActivator)); UseTypeActivator(new DelegateTypeActivator(createInstance));
/// <summary> /// <summary>
/// Configures the application to use the specified service provider for activating types. /// 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) => public CliApplicationBuilder UseTypeActivator(IServiceProvider serviceProvider) =>
UseTypeActivator(serviceProvider.GetService); UseTypeActivator(serviceProvider.GetService);
/// <summary>
/// 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.
/// </summary>
public CliApplicationBuilder UseTypeActivator(Func<IReadOnlyList<Type>, IServiceProvider> getServiceProvider) =>
UseTypeActivator(getServiceProvider(_commandTypes.ToArray()));
/// <summary> /// <summary>
/// Creates a configured instance of <see cref="CliApplication" />. /// Creates a configured instance of <see cref="CliApplication" />.
/// </summary> /// </summary>
@@ -221,45 +226,56 @@ public partial class CliApplicationBuilder
{ {
var entryAssemblyName = EnvironmentEx.EntryAssembly?.GetName().Name; var entryAssemblyName = EnvironmentEx.EntryAssembly?.GetName().Name;
if (string.IsNullOrWhiteSpace(entryAssemblyName)) 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; return entryAssemblyName;
} }
private static string GetDefaultExecutableName() private static string GetDefaultExecutableName()
{ {
var entryAssemblyLocation = EnvironmentEx.EntryAssembly?.Location; var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location;
if (string.IsNullOrWhiteSpace(entryAssemblyLocation)) var processFilePath = EnvironmentEx.ProcessPath;
return "app";
// If the application was launched via matching EXE apphost, use that as the executable name if (string.IsNullOrWhiteSpace(entryAssemblyFilePath) || string.IsNullOrWhiteSpace(processFilePath))
var isLaunchedViaAppHost = string.Equals( {
EnvironmentEx.ProcessPath, throw new InvalidOperationException(
Path.ChangeExtension(entryAssemblyLocation, ".exe"), "Failed to infer the default application executable name. " +
StringComparison.OrdinalIgnoreCase $"Please specify it explicitly using {nameof(SetExecutableName)}()."
); );
}
if (isLaunchedViaAppHost) // If the process path matches the entry assembly path, it's a legacy .NET Framework app
return Path.GetFileNameWithoutExtension(entryAssemblyLocation); // 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. // If the process path has the same name and parent directory as the entry assembly path,
// Prefix it with `dotnet` if it's a DLL file. // but different extension, it's a framework-dependent .NET Core app launched through the apphost.
var isDll = string.Equals( if (PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, ".exe"), processFilePath) ||
Path.GetExtension(entryAssemblyLocation), PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, ""), processFilePath))
".dll", {
StringComparison.OrdinalIgnoreCase return Path.GetFileNameWithoutExtension(entryAssemblyFilePath);
); }
return isDll // Otherwise, it's a framework-dependent .NET Core app launched through the .NET CLI
? "dotnet " + Path.GetFileName(entryAssemblyLocation) return "dotnet " + Path.GetFileName(entryAssemblyFilePath);
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
} }
private static string GetDefaultVersionText() private static string GetDefaultVersionText()
{ {
var entryAssemblyVersion = EnvironmentEx.EntryAssembly?.GetName().Version; var entryAssemblyVersion = EnvironmentEx.EntryAssembly?.GetName().Version;
if (entryAssemblyVersion is null) 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(); return "v" + entryAssemblyVersion.ToSemanticString();
} }

View File

@@ -4,22 +4,22 @@ using CliFx.Exceptions;
namespace CliFx.Infrastructure; namespace CliFx.Infrastructure;
/// <summary> /// <summary>
/// Implementation of <see cref="ITypeActivator" /> that instantiates an object /// Implementation of <see cref="ITypeActivator" /> that instantiates an object by using a predefined delegate.
/// by using a predefined function.
/// </summary> /// </summary>
public class DelegateTypeActivator : ITypeActivator public class DelegateTypeActivator : ITypeActivator
{ {
private readonly Func<Type, object> _func; private readonly Func<Type, object> _createInstance;
/// <summary> /// <summary>
/// Initializes an instance of <see cref="DelegateTypeActivator" />. /// Initializes an instance of <see cref="DelegateTypeActivator" />.
/// </summary> /// </summary>
public DelegateTypeActivator(Func<Type, object> func) => _func = func; public DelegateTypeActivator(Func<Type, object> createInstance) =>
_createInstance = createInstance;
/// <inheritdoc /> /// <inheritdoc />
public object CreateInstance(Type type) public object CreateInstance(Type type)
{ {
var instance = _func(type); var instance = _createInstance(type);
if (instance is null) if (instance is null)
{ {

22
CliFx/Utils/PathEx.cs Normal file
View File

@@ -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));
}
}

View File

@@ -531,24 +531,24 @@ The following example shows how to configure your application to use [`Microsoft
```csharp ```csharp
public static class Program public static class Program
{ {
public static async Task<int> Main() public static async Task<int> Main() =>
{ await new CliApplicationBuilder()
var services = new ServiceCollection();
// Register services
services.AddSingleton<MyService>();
// Register commands
services.AddTransient<MyCommand>();
var serviceProvider = services.BuildServiceProvider();
return await new CliApplicationBuilder()
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseTypeActivator(serviceProvider) .UseTypeActivator(commandTypes =>
{
var services = new ServiceCollection();
// Register services
services.AddSingleton<MyService>();
// Register commands
foreach (var commandType in commandTypes)
services.AddTransient(commandType);
return services.BuildServiceProvider();
})
.Build() .Build()
.RunAsync(); .RunAsync();
}
} }
``` ```