From 211abe1b4cd58d7e3fb743c1a736f4e27c4b48bc Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:44:03 -0400 Subject: [PATCH] Add support for keyed DI and update target frameworks Introduced `FromKeyedServicesAttribute` for resolving services by key in method parameters. Updated `ReflectionHelper` to handle keyed service resolution using `IKeyedServiceProvider`. Added tests to validate this functionality. Upgraded target frameworks to `net8.0` and `net9.0` across projects. Simplified reflection code by replacing `Array.Empty()` with `[]`. Updated dependencies to use `Microsoft.Extensions.DependencyInjection.Abstractions` and newer versions of related packages. Enhanced test coverage with new tests for keyed DI and fixed typos in test method names. Adjusted project files to include additional package references and modernized exception messages for consistency. --- .../Internal/ReflectionHelper.cs | 24 ++++++++-- ...cMaster.Extensions.CommandLineUtils.csproj | 6 ++- ...ster.Extensions.Hosting.CommandLine.csproj | 4 +- .../CommandLineApplicationOfTTests.cs | 6 +-- .../ExecuteMethodConventionTests.cs | 48 +++++++++++++++++++ ...r.Extensions.CommandLineUtils.Tests.csproj | 6 ++- ...xtensions.Hosting.CommandLine.Tests.csproj | 2 +- 7 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index 07b6daec..b8f6b531 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading; using McMaster.Extensions.CommandLineUtils.Abstractions; +using Microsoft.Extensions.DependencyInjection; namespace McMaster.Extensions.CommandLineUtils { @@ -21,7 +22,7 @@ public static SetPropertyDelegate GetPropertySetter(PropertyInfo prop) var setter = prop.GetSetMethod(nonPublic: true); if (setter != null) { - return (obj, value) => setter.Invoke(obj, new object?[] { value }); + return (obj, value) => setter.Invoke(obj, [value]); } else { @@ -44,7 +45,7 @@ public static GetPropertyDelegate GetPropertyGetter(PropertyInfo prop) if (getter != null) { #pragma warning disable CS8603 // Possible null reference return. - return obj => getter.Invoke(obj, Array.Empty()); + return obj => getter.Invoke(obj, []); #pragma warning restore CS8603 // Possible null reference return. } else @@ -122,8 +123,23 @@ public static MemberInfo[] GetMembers(Type type) } else { - var service = command.AdditionalServices?.GetService(methodParam.ParameterType); - arguments[i] = service ?? throw new InvalidOperationException(Strings.UnsupportedParameterTypeOnMethod(method.Name, methodParam)); + // Check for FromKeyedServicesAttribute + var keyedAttr = methodParam.GetCustomAttribute(); + if (keyedAttr != null) + { + if (command.AdditionalServices is not IKeyedServiceProvider keyedServiceProvider) + { + throw new InvalidOperationException("AdditionalServices does not support keyed service resolution."); + } + + arguments[i] = keyedServiceProvider.GetKeyedService(methodParam.ParameterType, keyedAttr.Key) + ?? throw new InvalidOperationException($"No keyed service found for type {methodParam.ParameterType} and key '{keyedAttr.Key}'."); + } + else + { + var service = command.AdditionalServices?.GetService(methodParam.ParameterType); + arguments[i] = service ?? throw new InvalidOperationException(Strings.UnsupportedParameterTypeOnMethod(method.Name, methodParam)); + } } } diff --git a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj index d3f8037a..f5dca581 100644 --- a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj +++ b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 true true Command-line parsing API. @@ -25,4 +25,8 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper + + + + diff --git a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj index af94d444..d26014ba 100644 --- a/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj +++ b/src/Hosting.CommandLine/McMaster.Extensions.Hosting.CommandLine.csproj @@ -1,7 +1,7 @@ - + - net6.0 + net8.0 true true Provides command-line parsing API integration with the generic host API (Microsoft.Extensions.Hosting). diff --git a/test/CommandLineUtils.Tests/CommandLineApplicationOfTTests.cs b/test/CommandLineUtils.Tests/CommandLineApplicationOfTTests.cs index 1696b379..50f72144 100644 --- a/test/CommandLineUtils.Tests/CommandLineApplicationOfTTests.cs +++ b/test/CommandLineUtils.Tests/CommandLineApplicationOfTTests.cs @@ -95,7 +95,7 @@ class ThrowsInCtorClass { public ThrowsInCtorClass() { - throw new XunitException("Parent comand object should not be initialized.\n" + Environment.StackTrace); + throw new XunitException("Parent command object should not be initialized.\n" + Environment.StackTrace); } public void OnExecute() { } @@ -124,7 +124,7 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionWhenHasSubcommand() } [Fact] - public void ItDoesNotInitalizeClassUnlessNecessary() + public void ItDoesNotInitializeClassUnlessNecessary() { using var app = new CommandLineApplication(new TestConsole(_output)); app.Conventions.UseDefaultConventions(); @@ -139,7 +139,7 @@ class SimpleCommand } [Fact] - public void ItDoesNotInitalizeParentClassUnlessNecessary() + public void ItDoesNotInitializeParentClassUnlessNecessary() { using var app = new CommandLineApplication(new TestConsole(_output)); app.Conventions.UseDefaultConventions(); diff --git a/test/CommandLineUtils.Tests/ExecuteMethodConventionTests.cs b/test/CommandLineUtils.Tests/ExecuteMethodConventionTests.cs index a283c818..c19397d8 100644 --- a/test/CommandLineUtils.Tests/ExecuteMethodConventionTests.cs +++ b/test/CommandLineUtils.Tests/ExecuteMethodConventionTests.cs @@ -4,10 +4,14 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Moq; using Xunit; using Xunit.Abstractions; +using McMaster.Extensions.CommandLineUtils; + namespace McMaster.Extensions.CommandLineUtils.Tests { public class ExecuteMethodConventionTests @@ -141,5 +145,49 @@ public async Task ItExecutesAsyncMethod() Assert.Equal(4, result); Assert.True(app.Model.Token.IsCancellationRequested); } + + + + private class MyClass(string name) + { + public string Name { get; } = name; + } + + private class ProgramWithExecuteAndKeyedArgumentInjection + { + private int OnExecute + ( + [FromKeyedServices("Database1")] MyClass myClass1, + [FromKeyedServices("Database2")] MyClass myClass2, + string nonKeyedArgument + ) + { + Assert.Equal("MyClass1", myClass1.Name); + Assert.Equal("MyClass2", myClass2.Name); + Assert.Equal("42", nonKeyedArgument); + return 42; + } + } + + [Fact] + public void OnExecuteWithKeyedArgumentsResolvesArgumentsByKey() + { + var serviceCollection = new ServiceCollection(); + serviceCollection + .AddKeyedSingleton("Database1", new MyClass("MyClass1")) + .AddKeyedSingleton("Database2", new MyClass("MyClass2")) + .AddSingleton("42") + ; + + var app = new CommandLineApplication(); + + app.AdditionalServices = serviceCollection.BuildServiceProvider(); + + app.Conventions.UseOnExecuteMethodFromModel(); + var result = app.Execute(); + Assert.Equal(42, result); + } + + } } diff --git a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj index 1ab1af5f..1a166a7c 100644 --- a/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj +++ b/test/CommandLineUtils.Tests/McMaster.Extensions.CommandLineUtils.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net8.0;net9.0 annotations @@ -13,7 +13,9 @@ - + + + diff --git a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj index 5e19f487..9c62798c 100644 --- a/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj +++ b/test/Hosting.CommandLine.Tests/McMaster.Extensions.Hosting.CommandLine.Tests.csproj @@ -1,7 +1,7 @@  - net8.0;net6.0 + net8.0;net9.0