Add the possibility to register multiple interceptors (#1412)

Having the interceptors registered with the ITypeRegistrar also enables the usage of ITypeResolver in interceptors.
This commit is contained in:
Nils Andresen 2024-01-06 23:28:20 +01:00 committed by GitHub
parent e7ce6a69b7
commit a94bc15746
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 7 deletions

View File

@ -81,9 +81,12 @@ Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolv
there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown.
## Interception
Interceptors can be registered with the `TypeRegistrar` (or with a custom DI-Container). Alternatively, `CommandApp` also provides a `SetInterceptor` configuration.
`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is run before all commands are executed. This is typically used for configuring logging or other infrastructure concerns.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, The `Intercept`-Method of an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
When the command has been run, the `InterceptResult`-Method of the same instance is called with the result of the command.
This provides an opportunity to modify the result and also to tear down any infrastructure in use.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
The `Intercept`-Method of each interceptor is run before the command is executed and the `InterceptResult`-Method is run after it. These are typically used for configuring logging or other infrastructure concerns.
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Logging).

View File

@ -229,7 +229,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.Interceptor = interceptor;
configurator.Settings.Registrar.RegisterInstance<ICommandInterceptor>(interceptor);
return configurator;
}

View File

@ -50,6 +50,7 @@ public interface ICommandAppSettings
/// Gets or sets the <see cref="ICommandInterceptor"/> used
/// to intercept settings before it's being sent to the command.
/// </summary>
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
ICommandInterceptor? Interceptor { get; set; }
/// <summary>

View File

@ -12,5 +12,25 @@ public interface ICommandInterceptor
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
void Intercept(CommandContext context, CommandSettings settings);
void Intercept(CommandContext context, CommandSettings settings)
#if NETSTANDARD2_0
;
#else
{
}
#endif
/// <summary>
/// Intercepts a command result before it's passed as the result.
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
/// <param name="result">The result from the command execution.</param>
void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
#if NETSTANDARD2_0
;
#else
{
}
#endif
}

View File

@ -121,7 +121,7 @@ internal sealed class CommandExecutor
VersionHelper.GetVersion(Assembly.GetEntryAssembly());
}
private static Task<int> Execute(
private static async Task<int> Execute(
CommandTree leaf,
CommandTree tree,
CommandContext context,
@ -130,7 +130,19 @@ internal sealed class CommandExecutor
{
// Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
configuration.Settings.Interceptor?.Intercept(context, settings);
var interceptors =
((IEnumerable<ICommandInterceptor>?)resolver.Resolve(typeof(IEnumerable<ICommandInterceptor>))
?? Array.Empty<ICommandInterceptor>()).ToList();
#pragma warning disable CS0618 // Type or member is obsolete
if (configuration.Settings.Interceptor != null)
{
interceptors.Add(configuration.Settings.Interceptor);
}
#pragma warning restore CS0618 // Type or member is obsolete
foreach (var interceptor in interceptors)
{
interceptor.Intercept(context, settings);
}
// Create and validate the command.
var command = leaf.CreateCommand(resolver);
@ -141,6 +153,12 @@ internal sealed class CommandExecutor
}
// Execute the command.
return command.Execute(context, settings);
var result = await command.Execute(context, settings);
foreach (var interceptor in interceptors)
{
interceptor.InterceptResult(context, settings, ref result);
}
return result;
}
}

View File

@ -8,6 +8,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
public CaseSensitivity CaseSensitivity { get; set; }

View File

@ -21,4 +21,11 @@ public sealed class CallbackCommandInterceptor : ICommandInterceptor
{
_callback(context, settings);
}
#if NETSTANDARD2_0
/// <inheritdoc/>
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
}
#endif
}

View File

@ -0,0 +1,90 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed class Interceptor
{
public sealed class NoCommand : Command<NoCommand.Settings>
{
public sealed class Settings : CommandSettings
{
}
public override int Execute(CommandContext context, Settings settings)
{
return 0;
}
}
public sealed class MyInterceptor : ICommandInterceptor
{
private readonly Action<CommandContext, CommandSettings> _action;
public MyInterceptor(Action<CommandContext, CommandSettings> action)
{
_action = action;
}
public void Intercept(CommandContext context, CommandSettings settings)
{
_action(context, settings);
}
}
public sealed class MyResultInterceptor : ICommandInterceptor
{
private readonly Func<CommandContext, CommandSettings, int, int> _function;
public MyResultInterceptor(Func<CommandContext, CommandSettings, int, int> function)
{
_function = function;
}
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
result = _function(context, settings, result);
}
}
[Fact]
public void Should_Run_The_Interceptor()
{
// Given
var count = 0;
var app = new CommandApp<NoCommand>();
var interceptor = new MyInterceptor((_, _) =>
{
count += 1;
});
app.Configure(config => config.SetInterceptor(interceptor));
// When
app.Run(Array.Empty<string>());
// Then
count.ShouldBe(1); // to be sure
}
[Fact]
public void Should_Run_The_ResultInterceptor()
{
// Given
var count = 0;
const int Expected = 123;
var app = new CommandApp<NoCommand>();
var interceptor = new MyResultInterceptor((_, _, _) =>
{
count += 1;
return Expected;
});
app.Configure(config => config.SetInterceptor(interceptor));
// When
var actual = app.Run(Array.Empty<string>());
// Then
count.ShouldBe(1);
actual.ShouldBe(Expected);
}
}
}