Skip to content

Commit e6678a9

Browse files
committed
Merge remote-tracking branch 'origin/release/v5.0.1'
2 parents a2a6317 + ab6d9b2 commit e6678a9

20 files changed

Lines changed: 875 additions & 91 deletions

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020

2121
- Removed obsolete serialization constructor and `[Serializable]` attribute from `DotNetConsoleException` as formatter-based serialization is no longer supported or recommended in modern .NET.
2222

23+
## [5.0.1] - 2026-03-25
24+
25+
### Added
26+
27+
- `ConfigureAppConfiguration` method to `DotNetConsoleBuilder` (re-)enabling custom configuration providers
28+
- `CustomArgumentParsingAttribute` to allow verbs to accept unknown arguments for manual parsing
29+
30+
### Fixed
31+
32+
- Fixed configuration provider timing: `DotNetConsole.Configuration` now properly reflects all configuration providers (including those added via `ConfigureAppConfiguration`) when accessed early in the application lifecycle.
33+
2334
## [5.0.0] - 2025-01-27
2435

2536
### Added
@@ -120,7 +131,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
120131

121132
- Removed AWS NLog Logger assembly that was used for logging to Amazon CloudWatch
122133

123-
[unreleased]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v5.0.0...HEAD
134+
[Unreleased]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v5.0.1...HEAD
124135
[3.0.1]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v3.0.0...v3.0.1
125136
[3.0.0]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v2.0.2...v3.0.0
126137
[2.0.2]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v2.0.1...v2.0.2
@@ -131,3 +142,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
131142
[3.0.2]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v3.0.2-rc.0...v3.0.2
132143
[5.0.0]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v4.0.0...v5.0.0
133144
[4.0.0]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v4.0.0-rc.0...v4.0.0
145+
[5.0.1]: https://github.com/neolution-ch/Neolution.DotNet.Console/compare/v5.0.1-beta.2...v5.0.1

Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Reflection;
6+
using Microsoft.Extensions.Configuration;
67
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Hosting;
79
using Neolution.DotNet.Console.UnitTests.Fakes;
810
using Neolution.DotNet.Console.UnitTests.Stubs;
911
using Shouldly;
@@ -113,5 +115,135 @@ public void GivenCheckDependenciesCommand_WhenRegistrationIsMissing_ThenShouldTh
113115
// Assert
114116
Should.Throw(() => builder.Build(), typeof(AggregateException));
115117
}
118+
119+
/// <summary>
120+
/// Given a null configuration delegate, when calling ConfigureAppConfiguration, then should throw ArgumentNullException.
121+
/// </summary>
122+
[Fact]
123+
public void GivenNullConfigurationDelegate_WhenCallingConfigureAppConfiguration_ThenShouldThrowArgumentNullException()
124+
{
125+
// Arrange
126+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
127+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, []);
128+
129+
// Act & Assert
130+
Should.Throw<ArgumentNullException>(() => builder.ConfigureAppConfiguration(null!));
131+
}
132+
133+
/// <summary>
134+
/// Given a valid configuration delegate, when calling ConfigureAppConfiguration, then should return the builder instance.
135+
/// </summary>
136+
[Fact]
137+
public void GivenValidConfigurationDelegate_WhenCallingConfigureAppConfiguration_ThenShouldReturnBuilderInstance()
138+
{
139+
// Arrange
140+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
141+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, []);
142+
143+
// Act
144+
var result = builder.ConfigureAppConfiguration((context, config) => { });
145+
146+
// Assert
147+
result.ShouldBeSameAs(builder);
148+
}
149+
150+
/// <summary>
151+
/// Given a configuration delegate that adds a custom configuration source, when building the console, then should be able to access the custom configuration.
152+
/// </summary>
153+
[Fact]
154+
public void GivenCustomConfigurationDelegate_WhenBuildingConsole_ThenShouldApplyCustomConfiguration()
155+
{
156+
// Arrange
157+
const string customKey = "CustomTestKey";
158+
const string customValue = "CustomTestValue";
159+
160+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
161+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, [])
162+
.ConfigureAppConfiguration((context, config) =>
163+
{
164+
config.AddInMemoryCollection(new Dictionary<string, string?>
165+
{
166+
{ customKey, customValue },
167+
});
168+
});
169+
170+
// Act
171+
var console = builder.Build();
172+
173+
// Assert
174+
var configuration = console.Services.GetRequiredService<IConfiguration>();
175+
configuration[customKey].ShouldBe(customValue);
176+
}
177+
178+
/// <summary>
179+
/// Given multiple configuration delegates, when building the console, then should apply all delegates in order.
180+
/// </summary>
181+
[Fact]
182+
public void GivenMultipleConfigurationDelegates_WhenBuildingConsole_ThenShouldApplyAllDelegatesInOrder()
183+
{
184+
// Arrange
185+
const string firstKey = "FirstKey";
186+
const string firstValue = "FirstValue";
187+
const string secondKey = "SecondKey";
188+
const string secondValue = "SecondValue";
189+
const string overrideKey = "OverrideKey";
190+
const string initialValue = "InitialValue";
191+
const string finalValue = "FinalValue";
192+
193+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
194+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, [])
195+
.ConfigureAppConfiguration((context, config) =>
196+
{
197+
config.AddInMemoryCollection(new Dictionary<string, string?>
198+
{
199+
{ firstKey, firstValue },
200+
{ overrideKey, initialValue },
201+
});
202+
})
203+
.ConfigureAppConfiguration((context, config) =>
204+
{
205+
config.AddInMemoryCollection(new Dictionary<string, string?>
206+
{
207+
{ secondKey, secondValue },
208+
{ overrideKey, finalValue }, // This should override the previous value
209+
});
210+
});
211+
212+
// Act
213+
var console = builder.Build();
214+
215+
// Assert
216+
var configuration = console.Services.GetRequiredService<IConfiguration>();
217+
configuration[firstKey].ShouldBe(firstValue);
218+
configuration[secondKey].ShouldBe(secondValue);
219+
configuration[overrideKey].ShouldBe(finalValue); // Should be overridden by the second delegate
220+
}
221+
222+
/// <summary>
223+
/// Given a configuration delegate that accesses the HostBuilderContext, when building the console, then should have access to environment information.
224+
/// </summary>
225+
[Fact]
226+
public void GivenConfigurationDelegateThatAccessesContext_WhenBuildingConsole_ThenShouldHaveAccessToEnvironmentInformation()
227+
{
228+
// Arrange
229+
const string environmentKey = "CurrentEnvironment";
230+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
231+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, [])
232+
.ConfigureAppConfiguration((context, config) =>
233+
{
234+
config.AddInMemoryCollection(new Dictionary<string, string?>
235+
{
236+
{ environmentKey, context.HostingEnvironment.EnvironmentName },
237+
});
238+
});
239+
240+
// Act
241+
var console = builder.Build();
242+
243+
// Assert
244+
var configuration = console.Services.GetRequiredService<IConfiguration>();
245+
configuration[environmentKey].ShouldNotBeNullOrEmpty();
246+
configuration[environmentKey].ShouldBe("Production"); // Default environment
247+
}
116248
}
117249
}

Neolution.DotNet.Console.UnitTests/DotNetConsoleEnvironmentTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public async Task GivenEnvironmentNameEnvironmentVariable_WhenRunningConsoleApp_
3232
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", environmentName);
3333

3434
const string commandLineString = "default";
35-
var builder = DotNetConsole.CreateBuilderWithReference(Assembly.GetAssembly(typeof(DefaultCommand))!, commandLineString.Split(" "));
35+
var servicesAssembly = Assembly.GetAssembly(typeof(DefaultCommand))!;
36+
var verbTypes = new[] { typeof(DefaultOptions), typeof(EchoOptions) };
37+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, commandLineString.Split(" "));
3638

3739
if (builder.Environment.IsDevelopment())
3840
{

Neolution.DotNet.Console.UnitTests/DotNetConsoleGrammarTests.cs

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,140 @@ public async Task GivenBuiltConsoleApp_WhenCallingVerbWithScalarOption_ThenShoul
118118
options.Repeat.ShouldBe(2);
119119
}
120120

121+
/// <summary>
122+
/// When calling the console app with parameters but without specifying the default verb, it should run the default command with those parameters.
123+
/// </summary>
124+
/// <returns>The <see cref="Task"/>.</returns>
125+
[Fact]
126+
public async Task GivenBuiltConsoleApp_WhenCallingDefaultVerbWithParametersWithoutVerb_ThenShouldRunDefaultVerbWithParameters()
127+
{
128+
// Arrange
129+
const string args = "--option=Queue --tenantId=1234";
130+
var logger = new UnitTestLogger();
131+
var console = CreateConsoleAppWithLogger(args, logger);
132+
133+
// Act
134+
await console.RunAsync();
135+
136+
// Assert
137+
var options = (DefaultOptions)logger.LoggedObjects["options"];
138+
options.Option.ShouldBe("Queue");
139+
options.TenantId.ShouldBe("1234");
140+
}
141+
142+
/// <summary>
143+
/// When calling the console app with parameters and explicitly specifying the default verb, it should run the default command with those parameters.
144+
/// </summary>
145+
/// <returns>The <see cref="Task"/>.</returns>
146+
[Fact]
147+
public async Task GivenBuiltConsoleApp_WhenCallingDefaultVerbWithParametersWithVerb_ThenShouldRunDefaultVerbWithParameters()
148+
{
149+
// Arrange
150+
const string args = "default --option=Queue --tenantId=1234";
151+
var logger = new UnitTestLogger();
152+
var console = CreateConsoleAppWithLogger(args, logger);
153+
154+
// Act
155+
await console.RunAsync();
156+
157+
// Assert
158+
var options = (DefaultOptions)logger.LoggedObjects["options"];
159+
options.Option.ShouldBe("Queue");
160+
options.TenantId.ShouldBe("1234");
161+
}
162+
163+
/// <summary>
164+
/// When calling the console app with parameters but without specifying the default verb, and the default options have no properties, it should still run the default command.
165+
/// This reproduces a customer scenario where they defined isDefault but parse arguments manually in the command.
166+
/// </summary>
167+
/// <returns>The <see cref="Task"/>.</returns>
168+
[Fact]
169+
public async Task GivenBuiltConsoleAppWithDefaultVerbWithoutProperties_WhenCallingWithParametersWithoutVerb_ThenShouldRunDefaultVerb()
170+
{
171+
// Arrange
172+
const string args = "--option=Queue --tenantId=1234";
173+
var logger = new UnitTestLogger();
174+
var console = CreateConsoleAppWithLoggerForDefaultWithoutProperties(args, logger);
175+
176+
// Act
177+
await console.RunAsync();
178+
179+
// Assert
180+
logger.LoggedObjects["options"].ShouldBeOfType<DefaultOptionsWithoutProperties>();
181+
}
182+
183+
/// <summary>
184+
/// When calling a non-default verb with custom argument parsing and unknown arguments, it should succeed.
185+
/// </summary>
186+
/// <returns>The <see cref="Task"/>.</returns>
187+
[Fact]
188+
public async Task GivenNonDefaultVerbWithCustomArgumentParsing_WhenCallingWithUnknownArgs_ThenShouldSucceed()
189+
{
190+
// Arrange
191+
const string args = "process --custom-arg=value --another-arg";
192+
var logger = new UnitTestLogger();
193+
var servicesAssembly = Assembly.GetAssembly(typeof(ProcessCommand))!;
194+
var verbTypes = new[] { typeof(ProcessOptions) };
195+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" "));
196+
builder.Services.Replace(new ServiceDescriptor(typeof(IUnitTestLogger), logger));
197+
var console = builder.Build();
198+
199+
// Act
200+
await console.RunAsync();
201+
202+
// Assert
203+
logger.LoggedObjects["options"].ShouldBeOfType<ProcessOptions>();
204+
}
205+
206+
/// <summary>
207+
/// When calling a verb with custom argument parsing and some defined properties, it should parse known args and ignore unknown ones.
208+
/// </summary>
209+
/// <returns>The <see cref="Task"/>.</returns>
210+
[Fact]
211+
public async Task GivenVerbWithCustomArgumentParsingAndProperties_WhenCallingWithMixedArgs_ThenShouldParseKnownAndIgnoreUnknown()
212+
{
213+
// Arrange
214+
const string args = "mixed --name=Test --count=5 --unknown-arg=ignored --another=value";
215+
var logger = new UnitTestLogger();
216+
var servicesAssembly = Assembly.GetAssembly(typeof(MixedCommand))!;
217+
var verbTypes = new[] { typeof(MixedOptions) };
218+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" "));
219+
builder.Services.Replace(new ServiceDescriptor(typeof(IUnitTestLogger), logger));
220+
var console = builder.Build();
221+
222+
// Act
223+
await console.RunAsync();
224+
225+
// Assert
226+
var options = (MixedOptions)logger.LoggedObjects["options"];
227+
options.Name.ShouldBe("Test");
228+
options.Count.ShouldBe(5);
229+
}
230+
231+
/// <summary>
232+
/// When calling a verb without custom argument parsing attribute and unknown arguments, the command should not execute.
233+
/// This ensures the default behavior still works.
234+
/// </summary>
235+
/// <returns>The <see cref="Task"/>.</returns>
236+
[Fact]
237+
public async Task GivenVerbWithoutCustomArgumentParsing_WhenCallingWithUnknownArgs_ThenCommandShouldNotExecute()
238+
{
239+
// Arrange
240+
const string args = "echo --unknown-option=value";
241+
var logger = new UnitTestLogger();
242+
var servicesAssembly = Assembly.GetAssembly(typeof(EchoCommand))!;
243+
var verbTypes = new[] { typeof(EchoOptions) };
244+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" "));
245+
builder.Services.Replace(new ServiceDescriptor(typeof(IUnitTestLogger), logger));
246+
var console = builder.Build();
247+
248+
// Act
249+
await console.RunAsync();
250+
251+
// Assert - command should not have executed, so logger should be empty
252+
logger.LoggedObjects.ShouldBeEmpty();
253+
}
254+
121255
/// <summary>
122256
/// Creates the console application with logger.
123257
/// </summary>
@@ -126,7 +260,26 @@ public async Task GivenBuiltConsoleApp_WhenCallingVerbWithScalarOption_ThenShoul
126260
/// <returns>A built console app ready to run.</returns>
127261
private static IDotNetConsole CreateConsoleAppWithLogger(string args, IUnitTestLogger tracker)
128262
{
129-
var builder = DotNetConsole.CreateBuilderWithReference(Assembly.GetAssembly(typeof(DefaultCommand))!, args.Split(" "));
263+
var servicesAssembly = Assembly.GetAssembly(typeof(DefaultCommand))!;
264+
var verbTypes = new[] { typeof(DefaultOptions), typeof(EchoOptions) };
265+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" "));
266+
267+
builder.Services.Replace(new ServiceDescriptor(typeof(IUnitTestLogger), tracker));
268+
269+
return builder.Build();
270+
}
271+
272+
/// <summary>
273+
/// Creates the console application with logger for default command without properties.
274+
/// </summary>
275+
/// <param name="args">The arguments.</param>
276+
/// <param name="tracker">The logger.</param>
277+
/// <returns>A built console app ready to run.</returns>
278+
private static IDotNetConsole CreateConsoleAppWithLoggerForDefaultWithoutProperties(string args, IUnitTestLogger tracker)
279+
{
280+
var servicesAssembly = Assembly.GetAssembly(typeof(DefaultCommandWithoutProperties))!;
281+
var verbTypes = new[] { typeof(DefaultOptionsWithoutProperties) };
282+
var builder = DotNetConsole.CreateBuilderWithReference(servicesAssembly, verbTypes, args.Split(" "));
130283

131284
builder.Services.Replace(new ServiceDescriptor(typeof(IUnitTestLogger), tracker));
132285

0 commit comments

Comments
 (0)