Skip to content

Commit 48e4cd9

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 9e17073 + 70aa69e commit 48e4cd9

File tree

10 files changed

+245
-29
lines changed

10 files changed

+245
-29
lines changed

ReadMe.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ This converting behaviour can configure by `ConsoleAppOptions.NameConverter`.
365365

366366
ConsoleApp / ConsoleAppBuilder
367367
---
368-
`ConsoleApp` is an entrypoint of creating ConsoleAppFramework app. It has three APIs, `Create`, `CreateBuilder` and `Run`.
368+
`ConsoleApp` is an entrypoint of creating ConsoleAppFramework app. It has three APIs, `Create`, `CreateBuilder`, `CreateFromHostBuilder` and `Run`.
369369

370370
```csharp
371371
// Create is shorthand of CraeteBuilder(args).Build();
@@ -387,7 +387,7 @@ ConsoleApp.Run(args, /* lambda expression */);
387387
ConsoleApp.Run<MyCommands>(args);
388388
```
389389

390-
When calling `Create/CreateBuilder`, also configure `ConsoleAppOptions`. Full option details, see [ConsoleAppOptions](#consoleappoptions) section.
390+
When calling `Create/CreateBuilder/CreateFromHostBuilder`, also configure `ConsoleAppOptions`. Full option details, see [ConsoleAppOptions](#consoleappoptions) section.
391391

392392
```csharp
393393
var app = ConsoleApp.Create(args, options =>
@@ -397,6 +397,16 @@ var app = ConsoleApp.Create(args, options =>
397397
});
398398
```
399399

400+
Advanced API of `ConsoleApp`, `CreateFromHostBuilder` creates ConsoleApp from IHostBuilder.
401+
402+
```csharp
403+
// Setup services outside of ConsoleAppFramework.
404+
var hostBuilder = Host.CreateDefaultBuilder()
405+
.ConfigureServices();
406+
407+
var app = ConsoleApp.CreateFromHostBuilder(hostBuilder);
408+
```
409+
400410
`ConsoleAppBuilder` itself is `IHostBuilder` so you can use any configuration methods like `ConfigureServices`, `ConfigureLogging`, etc. If method chain is not returns `ConsoleAppBuilder`(for example, using external lib's extension methods), can not get `ConsoleApp` directly. In that case, use `BuildAsConsoleApp()` instead of `Build()`.
401411

402412
`ConsoleApp` exposes some utility properties.

src/ConsoleAppFramework/CommandHelpBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public string BuildHelpMessage(CommandDescriptor? defaultCommand, IEnumerable<Co
4141
sb.Append(BuildHelpMessage(CreateCommandHelpDefinition(defaultCommand, shortCommandName), showCommandName: false, fromMultiCommand: false));
4242
}
4343

44-
var orderedCommands = commands.OrderBy(x => x.GetNamesFormatted(options)).ToArray();
44+
var orderedCommands = options.HelpSortCommandsByFullName
45+
? commands.OrderBy(x => x.GetCommandName(options)).ToArray()
46+
: commands.OrderBy(x => x.GetNamesFormatted(options)).ToArray();
4547
if (orderedCommands.Length > 0)
4648
{
4749
if (defaultCommand == null)

src/ConsoleAppFramework/ConsoleApp.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,32 @@ public static ConsoleApp Create(string[] args, Action<HostBuilderContext, Consol
5151

5252
public static ConsoleAppBuilder CreateBuilder(string[] args)
5353
{
54-
return new ConsoleAppBuilder(args);
54+
return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args));
5555
}
5656

5757
public static ConsoleAppBuilder CreateBuilder(string[] args, Action<ConsoleAppOptions> configureOptions)
5858
{
59-
return new ConsoleAppBuilder(args, configureOptions);
59+
return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args), configureOptions);
6060
}
6161

6262
public static ConsoleAppBuilder CreateBuilder(string[] args, Action<HostBuilderContext, ConsoleAppOptions> configureOptions)
6363
{
64-
return new ConsoleAppBuilder(args, configureOptions);
64+
return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args), configureOptions);
65+
}
66+
67+
public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args)
68+
{
69+
return new ConsoleAppBuilder(args, hostBuilder).Build();
70+
}
71+
72+
public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args, Action<ConsoleAppOptions> configureOptions)
73+
{
74+
return new ConsoleAppBuilder(args, hostBuilder, configureOptions).Build();
75+
}
76+
77+
public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args, Action<HostBuilderContext, ConsoleAppOptions> configureOptions)
78+
{
79+
return new ConsoleAppBuilder(args, hostBuilder, configureOptions).Build();
6580
}
6681

6782
public static void Run(string[] args, Delegate rootCommand)

src/ConsoleAppFramework/ConsoleAppBuilder.cs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,26 @@ public class ConsoleAppBuilder : IHostBuilder
1111
{
1212
readonly IHostBuilder builder;
1313

14-
internal ConsoleAppBuilder(string[] args)
15-
: this(args, (_, __) => { })
14+
internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder)
15+
: this(args, hostBuilder, (_, __) => { })
1616
{
1717
}
1818

19-
internal ConsoleAppBuilder(string[] args, ConsoleAppOptions consoleAppOptions)
19+
internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, ConsoleAppOptions consoleAppOptions)
2020
{
21-
var hostBuilder = new HostBuilder();
22-
hostBuilder.ConfigureDefaults(args);
2321
this.builder = AddConsoleAppFramework(hostBuilder, args, consoleAppOptions, null);
2422
}
2523

26-
internal ConsoleAppBuilder(string[] args, Action<ConsoleAppOptions> configureOptions)
27-
: this(args, (_, options) => configureOptions(options))
24+
internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, Action<ConsoleAppOptions> configureOptions)
25+
: this(args, hostBuilder, (_, options) => configureOptions(options))
2826
{
2927
}
3028

31-
internal ConsoleAppBuilder(string[] args, Action<HostBuilderContext, ConsoleAppOptions> configureOptions)
29+
internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, Action<HostBuilderContext, ConsoleAppOptions> configureOptions)
3230
{
33-
var hostBuilder = new HostBuilder();
34-
hostBuilder.ConfigureDefaults(args);
3531
this.builder = AddConsoleAppFramework(hostBuilder, args, new ConsoleAppOptions(), configureOptions);
3632
}
3733

38-
// internal use for legacy compatible
39-
internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, ConsoleAppOptions options)
40-
{
41-
this.builder = AddConsoleAppFramework(hostBuilder, args, options, (_, __) => { });
42-
}
43-
4434
IHostBuilder AddConsoleAppFramework(IHostBuilder builder, string[] args, ConsoleAppOptions options, Action<HostBuilderContext, ConsoleAppOptions>? configureOptions)
4535
{
4636
return builder
@@ -51,6 +41,7 @@ IHostBuilder AddConsoleAppFramework(IHostBuilder builder, string[] args, Console
5141
configureOptions?.Invoke(ctx, options);
5242
options.CommandLineArguments = args;
5343
services.AddSingleton(options);
44+
services.AddSingleton<IParamsValidator, ParamsValidator>();
5445

5546
if (options.ReplaceToUseSimpleConsoleLogger)
5647
{

src/ConsoleAppFramework/ConsoleAppEngine.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using Microsoft.Extensions.Logging;
33
using System;
44
using System.Collections.Generic;
5+
using System.Collections.Immutable;
56
using System.Collections.ObjectModel;
7+
using System.ComponentModel.DataAnnotations;
68
using System.Linq;
79
using System.Reflection;
810
using System.Text.Json;
@@ -18,12 +20,19 @@ internal class ConsoleAppEngine
1820
readonly CancellationTokenSource cancellationTokenSource;
1921
readonly ConsoleAppOptions options;
2022
readonly IServiceProviderIsService isService;
23+
readonly IParamsValidator paramsValidator;
2124
readonly bool isStrict;
2225

23-
public ConsoleAppEngine(ILogger<ConsoleApp> logger, IServiceProvider provider, ConsoleAppOptions options, IServiceProviderIsService isService, CancellationTokenSource cancellationTokenSource)
26+
public ConsoleAppEngine(ILogger<ConsoleApp> logger,
27+
IServiceProvider provider,
28+
ConsoleAppOptions options,
29+
IServiceProviderIsService isService,
30+
IParamsValidator paramsValidator,
31+
CancellationTokenSource cancellationTokenSource)
2432
{
2533
this.logger = logger;
2634
this.provider = provider;
35+
this.paramsValidator = paramsValidator;
2736
this.cancellationTokenSource = cancellationTokenSource;
2837
this.options = options;
2938
this.isService = isService;
@@ -168,6 +177,13 @@ async Task RunCore(Type type, MethodInfo methodInfo, object? instance, string?[]
168177
invokeArgs = newInvokeArgs;
169178
}
170179

180+
var validationResult = paramsValidator.ValidateParameters(originalParameters.Zip(invokeArgs));
181+
if (validationResult != ValidationResult.Success)
182+
{
183+
await SetFailAsync(validationResult!.ErrorMessage!);
184+
return;
185+
}
186+
171187
try
172188
{
173189
if (instance == null && !type.IsAbstract && !methodInfo.IsStatic)

src/ConsoleAppFramework/ConsoleAppFilter.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,9 @@ public WithFilterInvoker(MethodInfo methodInfo, object? instance, object?[] invo
7575
var methodFilters = methodInfo.GetCustomAttributes<ConsoleAppFilterAttribute>(true);
7676
foreach (var item in classFilters.Concat(methodFilters))
7777
{
78-
var filter = ActivatorUtilities.CreateInstance<ConsoleAppFilter>(serviceProvider, item.Type);
79-
if (filter != null)
80-
{
81-
filter.Order = item.Order;
82-
list.Add(filter);
83-
}
78+
var filter = (ConsoleAppFilter) ActivatorUtilities.CreateInstance(serviceProvider, item.Type);
79+
filter.Order = item.Order;
80+
list.Add(filter);
8481
}
8582

8683
var sortedAndReversedFilters = list.OrderBy(x => x.Order).Reverse().ToArray();

src/ConsoleAppFramework/ConsoleAppOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class ConsoleAppOptions
2424

2525
public Func<string, string> NameConverter { get; set; } = KebabCaseConvert;
2626

27+
public bool HelpSortCommandsByFullName { get; set; } = false;
28+
2729
// internal store values for execute engine.
2830

2931
internal string[] CommandLineArguments { get; set; } = default!;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.ComponentModel.DataAnnotations;
5+
using System.Linq;
6+
using System.Reflection;
7+
8+
namespace ConsoleAppFramework
9+
{
10+
/// <summary>
11+
/// Validator of command parameters.
12+
/// </summary>
13+
public interface IParamsValidator
14+
{
15+
/// <summary>
16+
/// Validate <paramref name="parameters"/> of command based on validation attributes
17+
/// applied to method's parameters.
18+
/// </summary>
19+
ValidationResult? ValidateParameters(IEnumerable<(ParameterInfo Parameter, object? Value)> parameters);
20+
}
21+
22+
/// <inheritdoc />
23+
public class ParamsValidator : IParamsValidator
24+
{
25+
private readonly ConsoleAppOptions options;
26+
27+
public ParamsValidator(ConsoleAppOptions options) => this.options = options;
28+
29+
/// <inheritdoc />
30+
ValidationResult? IParamsValidator.ValidateParameters(
31+
IEnumerable<(ParameterInfo Parameter, object? Value)> parameters)
32+
{
33+
var invalidParameters = parameters
34+
.Select(tuple => (tuple.Parameter, tuple.Value, Result: Validate(tuple.Parameter, tuple.Value)))
35+
.Where(tuple => tuple.Result != ValidationResult.Success)
36+
.ToImmutableArray();
37+
38+
if (!invalidParameters.Any())
39+
{
40+
return ValidationResult.Success;
41+
}
42+
43+
var errorMessage = string.Join(Environment.NewLine,
44+
invalidParameters
45+
.Select(tuple =>
46+
$"{options.NameConverter(tuple.Parameter.Name!)} " +
47+
$"({tuple.Value}): " +
48+
$"{tuple.Result!.ErrorMessage}")
49+
);
50+
51+
return new ValidationResult($"Some parameters have invalid values:{Environment.NewLine}{errorMessage}");
52+
}
53+
54+
private static ValidationResult? Validate(ParameterInfo parameterInfo, object? value)
55+
{
56+
if (value is null) return ValidationResult.Success;
57+
58+
var validationContext = new ValidationContext(value, null, null);
59+
60+
var failedResults = GetValidationAttributes(parameterInfo)
61+
.Select(attribute => attribute.GetValidationResult(value, validationContext))
62+
.Where(result => result != ValidationResult.Success)
63+
.ToImmutableArray();
64+
65+
return failedResults.Any()
66+
? new ValidationResult(string.Join("; ", failedResults.Select(res => res?.ErrorMessage)))
67+
: ValidationResult.Success;
68+
}
69+
70+
private static IEnumerable<ValidationAttribute> GetValidationAttributes(ParameterInfo parameterInfo)
71+
=> parameterInfo
72+
.GetCustomAttributes()
73+
.OfType<ValidationAttribute>();
74+
}
75+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using FluentAssertions;
4+
using Microsoft.Extensions.Hosting;
5+
using Xunit;
6+
// ReSharper disable UnusedMember.Local
7+
// ReSharper disable ClassNeverInstantiated.Local
8+
9+
namespace ConsoleAppFramework.Integration.Test;
10+
11+
public class FilterTest
12+
{
13+
[Fact]
14+
public void ApplyAttributeFilterTest()
15+
{
16+
using var console = new CaptureConsoleOutput();
17+
var args = new[] { "test-argument-name" };
18+
Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync<TestConsoleApp>(args);
19+
console.Output.Should().Contain("[in filter] before");
20+
console.Output.Should().Contain(args[0]);
21+
console.Output.Should().Contain("[in filter] after");
22+
}
23+
24+
/// <inheritdoc />
25+
private class TestConsoleApp : ConsoleAppBase
26+
{
27+
[RootCommand]
28+
[ConsoleAppFilter(typeof(TestFilter))]
29+
public void RootCommand([Option(index: 0)]string someArgument) => Console.WriteLine(someArgument);
30+
}
31+
32+
/// <inheritdoc />
33+
private class TestFilter : ConsoleAppFilter
34+
{
35+
/// <inheritdoc />
36+
public override async ValueTask Invoke(ConsoleAppContext context, Func<ConsoleAppContext, ValueTask> next)
37+
{
38+
Console.WriteLine("[in filter] before");
39+
await next(context);
40+
Console.WriteLine("[in filter] after");
41+
}
42+
}
43+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
using FluentAssertions;
4+
using Xunit;
5+
6+
// ReSharper disable UnusedMember.Global
7+
// ReSharper disable UnusedParameter.Global
8+
9+
namespace ConsoleAppFramework.Integration.Test;
10+
11+
public class ValidationAttributeTests
12+
{
13+
/// <summary>
14+
/// Try to execute command with invalid option value.
15+
/// </summary>
16+
[Fact]
17+
public void Validate_String_Length_Test()
18+
{
19+
using var console = new CaptureConsoleOutput();
20+
21+
const string optionName = "arg";
22+
const string optionValue = "too-large-string-value";
23+
24+
var args = new[] { nameof(AppWithValidationAttributes.StrLength), $"--{optionName}", optionValue };
25+
ConsoleApp.Run<AppWithValidationAttributes>(args);
26+
27+
// Validation should fail, so StrLength command should not be executed.
28+
console.Output.Should().NotContain(AppWithValidationAttributes.Output);
29+
30+
console.Output.Should().Contain(optionName);
31+
console.Output.Should().Contain(optionValue);
32+
}
33+
34+
[Fact]
35+
public void Command_With_Multiple_Params()
36+
{
37+
using var console = new CaptureConsoleOutput();
38+
39+
var args = new[]
40+
{
41+
nameof(AppWithValidationAttributes.MultipleParams),
42+
"--second-arg", "10",
43+
"--first-arg", "invalid-email-address"
44+
};
45+
46+
ConsoleApp.Run<AppWithValidationAttributes>(args);
47+
48+
// Validation should fail, so StrLength command should not be executed.
49+
console.Output.Should().NotContain(AppWithValidationAttributes.Output);
50+
}
51+
52+
/// <inheritdoc />
53+
internal class AppWithValidationAttributes : ConsoleAppBase
54+
{
55+
public const string Output = $"hello from {nameof(AppWithValidationAttributes)}";
56+
57+
[Command(nameof(StrLength))]
58+
public void StrLength([StringLength(maximumLength: 8)] string arg) => Console.WriteLine(Output);
59+
60+
[Command(nameof(MultipleParams))]
61+
public void MultipleParams(
62+
[EmailAddress] string firstArg,
63+
[Range(0, 2)] int secondArg) => Console.WriteLine(Output);
64+
}
65+
}

0 commit comments

Comments
 (0)