Skip to content

Commit 0523049

Browse files
committed
Refactors internal console builder logic
Extracts configuration management and command-line processing into dedicated internal classes. This improves modularity and maintainability within the `DotNetConsoleBuilder`. The `ConsoleConfigurationManager` now handles configuration building and delegate management, centralizing dynamic configuration logic. The `CommandLineProcessor` encapsulates argument parsing and strict verb matching, making the core builder cleaner. Default host builder configuration, including logging and command registration, is moved to `ConsoleHostBuilderConfigurator`.
1 parent e2c7f44 commit 0523049

4 files changed

Lines changed: 274 additions & 137 deletions

File tree

Neolution.DotNet.Console/DotNetConsoleBuilder.cs

Lines changed: 12 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
{
33
using System;
44
using System.Collections.Generic;
5-
using System.Linq;
65
using System.Reflection;
76
using CommandLine;
87
using Microsoft.Extensions.Configuration;
98
using Microsoft.Extensions.DependencyInjection;
109
using Microsoft.Extensions.Hosting;
11-
using Microsoft.Extensions.Logging;
1210
using Neolution.DotNet.Console.Abstractions;
1311
using Neolution.DotNet.Console.Internal;
14-
using NLog.Extensions.Logging;
1512

1613
/// <summary>
1714
/// The console application builder.
@@ -34,24 +31,9 @@ public class DotNetConsoleBuilder
3431
private readonly ServiceCollection serviceCollection = new();
3532

3633
/// <summary>
37-
/// List of configuration delegates to be applied during host building.
34+
/// The configuration manager
3835
/// </summary>
39-
private readonly List<Action<HostBuilderContext, IConfigurationBuilder>> configurationDelegates = new();
40-
41-
/// <summary>
42-
/// The initial configuration builder used to create dynamic configuration
43-
/// </summary>
44-
private readonly IConfigurationBuilder configurationBuilder;
45-
46-
/// <summary>
47-
/// The host builder context used for dynamic configuration building
48-
/// </summary>
49-
private readonly HostBuilderContext hostBuilderContext;
50-
51-
/// <summary>
52-
/// The built configuration root - built once when first accessed
53-
/// </summary>
54-
private IConfigurationRoot? builtConfiguration;
36+
private readonly ConsoleConfigurationManager configurationManager;
5537

5638
/// <summary>
5739
/// Run only to check dependencies.
@@ -72,12 +54,14 @@ internal DotNetConsoleBuilder(IHostBuilder hostBuilder, ParserResult<object> com
7254
this.hostBuilder = hostBuilder;
7355
this.commandLineParserResult = commandLineParserResult;
7456
this.Environment = environment;
75-
this.configurationBuilder = configurationBuilder;
76-
this.hostBuilderContext = new HostBuilderContext(new Dictionary<object, object>())
57+
58+
var hostBuilderContext = new HostBuilderContext(new Dictionary<object, object>())
7759
{
7860
HostingEnvironment = environment,
7961
Configuration = configurationBuilder.Build(),
8062
};
63+
64+
this.configurationManager = new ConsoleConfigurationManager(configurationBuilder, hostBuilderContext);
8165
}
8266

8367
/// <summary>
@@ -88,35 +72,7 @@ internal DotNetConsoleBuilder(IHostBuilder hostBuilder, ParserResult<object> com
8872
/// <summary>
8973
/// Gets a collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers.
9074
/// </summary>
91-
public IConfiguration Configuration
92-
{
93-
get
94-
{
95-
// Build the configuration once when first accessed, similar to Microsoft's approach
96-
if (this.builtConfiguration == null)
97-
{
98-
// Create a new configuration builder based on the initial one
99-
var configBuilder = new ConfigurationBuilder();
100-
101-
// Add all sources from the initial configuration builder
102-
foreach (var source in this.configurationBuilder.Sources)
103-
{
104-
configBuilder.Add(source);
105-
}
106-
107-
// Apply all configuration delegates that have been added via ConfigureAppConfiguration
108-
foreach (var configureDelegate in this.configurationDelegates)
109-
{
110-
configureDelegate(this.hostBuilderContext, configBuilder);
111-
}
112-
113-
// Build and store the configuration root
114-
this.builtConfiguration = configBuilder.Build();
115-
}
116-
117-
return this.builtConfiguration;
118-
}
119-
}
75+
public IConfiguration Configuration => this.configurationManager.Configuration;
12076

12177
/// <summary>
12278
/// Gets the collection of services for the application to compose. This is useful for adding user provided or framework provided services.
@@ -130,13 +86,7 @@ public IConfiguration Configuration
13086
/// <returns>The <see cref="DotNetConsoleBuilder"/>.</returns>
13187
public DotNetConsoleBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
13288
{
133-
ArgumentNullException.ThrowIfNull(configureDelegate);
134-
135-
this.configurationDelegates.Add(configureDelegate);
136-
137-
// Reset the built configuration so it gets rebuilt on next access
138-
this.builtConfiguration = null;
139-
89+
this.configurationManager.AddConfigurationDelegate(configureDelegate);
14090
return this;
14191
}
14292

@@ -186,103 +136,28 @@ internal static DotNetConsoleBuilder CreateBuilderInternal(Assembly assembly, Ty
186136
var configBuilder = DotNetConsoleDefaults.CreateConsoleConfigurationBuilder(assembly, args, environment);
187137

188138
// Create a HostBuilder
189-
var builder = Host.CreateDefaultBuilder(args)
190-
.UseContentRoot(environment.ContentRootPath)
191-
.ConfigureLogging((context, logging) =>
192-
{
193-
AdjustDefaultBuilderLoggingProviders(logging);
194-
logging.AddNLog(context.Configuration);
195-
})
196-
.ConfigureServices((_, services) =>
197-
{
198-
// Register all commands found in the entry assembly.
199-
services.Scan(selector => selector.FromAssemblies(assembly)
200-
.AddClasses(classes => classes.AssignableTo(typeof(IDotNetConsoleCommand<>)))
201-
.AsImplementedInterfaces());
202-
});
139+
var builder = ConsoleHostBuilderConfigurator.CreateConfiguredHostBuilder(assembly, args, environment);
203140

204-
// If verb types were not specified, compile all available verbs for this run by looking for classes with the Verb attribute in the specified assembly
205-
verbTypes ??= assembly.GetTypes()
206-
.Where(t => t.GetCustomAttribute<VerbAttribute>() != null)
207-
.ToArray();
208-
209-
var parsedArguments = Parser.Default.ParseArguments(args, verbTypes);
141+
var parsedArguments = CommandLineProcessor.ParseArguments(assembly, verbTypes, args);
210142
var consoleBuilder = new DotNetConsoleBuilder(builder, parsedArguments, environment, configBuilder);
211143

212144
// Apply any custom configuration delegates that will be added later via ConfigureAppConfiguration
213145
builder.ConfigureAppConfiguration((context, configBuilder) =>
214146
{
215147
// Apply all stored configuration delegates
216-
foreach (var configureDelegate in consoleBuilder.configurationDelegates)
148+
foreach (var configureDelegate in consoleBuilder.configurationManager.ConfigurationDelegates)
217149
{
218150
configureDelegate(context, configBuilder);
219151
}
220152
});
221153

222-
if (args.Length == 1 && string.Equals(args[0], "check-deps", StringComparison.OrdinalIgnoreCase))
154+
if (CommandLineProcessor.IsCheckDependenciesRequest(args))
223155
{
224156
consoleBuilder.checkDependencies = true;
225157
return consoleBuilder;
226158
}
227159

228-
CheckStrictVerbMatching(args, verbTypes);
229160
return consoleBuilder;
230161
}
231-
232-
/// <summary>
233-
/// Adjusts the default builder logging providers.
234-
/// </summary>
235-
/// <param name="logging">The logging.</param>
236-
private static void AdjustDefaultBuilderLoggingProviders(ILoggingBuilder logging)
237-
{
238-
// Remove the default logging providers
239-
logging.ClearProviders();
240-
241-
// Re-add other logging providers that are assigned in Host.CreateDefaultBuilder
242-
logging.AddDebug();
243-
logging.AddEventSourceLogger();
244-
245-
if (OperatingSystem.IsWindows())
246-
{
247-
// Add the EventLogLoggerProvider on windows machines
248-
logging.AddEventLog();
249-
}
250-
}
251-
252-
/// <summary>
253-
/// Enforce strict verb matching if one verb is marked as default. Otherwise, the default verb will be executed even if that was not the users intention.
254-
/// </summary>
255-
/// <param name="args">The arguments.</param>
256-
/// <param name="availableVerbTypes">The available verb types.</param>
257-
/// <exception cref="Neolution.DotNet.Console.DotNetConsoleException">Cannot create builder, because the specified verb '{firstVerb}' matches no command.</exception>
258-
private static void CheckStrictVerbMatching(string[] args, Type[] availableVerbTypes)
259-
{
260-
var availableVerbs = availableVerbTypes.Select(t => t.GetCustomAttribute<VerbAttribute>()!).ToList();
261-
if (!availableVerbs.Any(v => v.IsDefault))
262-
{
263-
// If no default verb is defined, we do not enforce strict verb matching
264-
return;
265-
}
266-
267-
var firstVerb = args.FirstOrDefault();
268-
if (string.IsNullOrWhiteSpace(firstVerb) || firstVerb.StartsWith('-'))
269-
{
270-
// If the user passed no verb, but a default verb is defined, the default verb will be executed
271-
return;
272-
}
273-
274-
// Names reserved by CommandLineParser library
275-
var validFirstArguments = new List<string> { "--help", "--version", "help", "version" };
276-
277-
// Names of all available verbs
278-
validFirstArguments.AddRange(availableVerbs.Select(t => t.Name));
279-
280-
// Check if the first argument can be found in the list of valid arguments
281-
var verbMatched = validFirstArguments.Any(v => v.Equals(firstVerb, StringComparison.OrdinalIgnoreCase));
282-
if (!verbMatched)
283-
{
284-
throw new DotNetConsoleException($"Cannot create builder, because the specified verb '{firstVerb}' matches no command.");
285-
}
286-
}
287162
}
288163
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
namespace Neolution.DotNet.Console.Internal
2+
{
3+
using System;
4+
using System.Linq;
5+
using System.Reflection;
6+
using CommandLine;
7+
8+
/// <summary>
9+
/// Handles command line argument parsing and validation.
10+
/// </summary>
11+
internal static class CommandLineProcessor
12+
{
13+
/// <summary>
14+
/// Parses command line arguments using the available verb types.
15+
/// </summary>
16+
/// <param name="assembly">The assembly to scan for verb types.</param>
17+
/// <param name="verbTypes">The verb types to use for parsing. If null, will scan the assembly.</param>
18+
/// <param name="args">The command line arguments.</param>
19+
/// <returns>The parsed command line arguments.</returns>
20+
public static ParserResult<object> ParseArguments(Assembly assembly, Type[]? verbTypes, string[] args)
21+
{
22+
verbTypes ??= GetAvailableVerbTypes(assembly);
23+
24+
// Skip strict verb matching for special commands like check-deps
25+
if (!IsCheckDependenciesRequest(args))
26+
{
27+
CheckStrictVerbMatching(args, verbTypes);
28+
}
29+
30+
return Parser.Default.ParseArguments(args, verbTypes);
31+
}
32+
33+
/// <summary>
34+
/// Determines if the command line arguments represent a check dependencies request.
35+
/// </summary>
36+
/// <param name="args">The command line arguments.</param>
37+
/// <returns>True if this is a check dependencies request, false otherwise.</returns>
38+
public static bool IsCheckDependenciesRequest(string[] args)
39+
{
40+
return args.Length == 1 && string.Equals(args[0], "check-deps", StringComparison.OrdinalIgnoreCase);
41+
}
42+
43+
/// <summary>
44+
/// Gets all available verb types from the specified assembly.
45+
/// </summary>
46+
/// <param name="assembly">The assembly to scan.</param>
47+
/// <returns>Array of types that have the Verb attribute.</returns>
48+
private static Type[] GetAvailableVerbTypes(Assembly assembly)
49+
{
50+
return assembly.GetTypes()
51+
.Where(t => t.GetCustomAttribute<VerbAttribute>() != null)
52+
.ToArray();
53+
}
54+
55+
/// <summary>
56+
/// Enforce strict verb matching if one verb is marked as default. Otherwise, the default verb will be executed even if that was not the users intention.
57+
/// </summary>
58+
/// <param name="args">The arguments.</param>
59+
/// <param name="availableVerbTypes">The available verb types.</param>
60+
/// <exception cref="DotNetConsoleException">Cannot create builder, because the specified verb '{firstVerb}' matches no command.</exception>
61+
private static void CheckStrictVerbMatching(string[] args, Type[] availableVerbTypes)
62+
{
63+
var availableVerbs = availableVerbTypes.Select(t => t.GetCustomAttribute<VerbAttribute>()!).ToList();
64+
if (!availableVerbs.Any(v => v.IsDefault))
65+
{
66+
// If no default verb is defined, we do not enforce strict verb matching
67+
return;
68+
}
69+
70+
var firstVerb = args.FirstOrDefault();
71+
if (string.IsNullOrWhiteSpace(firstVerb) || firstVerb.StartsWith('-'))
72+
{
73+
// If the user passed no verb, but a default verb is defined, the default verb will be executed
74+
return;
75+
}
76+
77+
// Names reserved by CommandLineParser library
78+
var validFirstArguments = new[] { "--help", "--version", "help", "version" }
79+
.Concat(availableVerbs.Select(t => t.Name))
80+
.ToList();
81+
82+
// Check if the first argument can be found in the list of valid arguments
83+
var verbMatched = validFirstArguments.Any(v => v.Equals(firstVerb, StringComparison.OrdinalIgnoreCase));
84+
if (!verbMatched)
85+
{
86+
throw new DotNetConsoleException($"Cannot create builder, because the specified verb '{firstVerb}' matches no command.");
87+
}
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)