Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/CommandLineUtils/Internal/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Reflection;
using System.Threading;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using Microsoft.Extensions.DependencyInjection;

namespace McMaster.Extensions.CommandLineUtils
{
Expand All @@ -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
{
Expand All @@ -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<object>());
return obj => getter.Invoke(obj, []);
#pragma warning restore CS8603 // Possible null reference return.
}
else
Expand Down Expand Up @@ -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<FromKeyedServicesAttribute>();
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));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<Description>Command-line parsing API.</Description>
Expand All @@ -25,4 +25,8 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<Description>Provides command-line parsing API integration with the generic host API (Microsoft.Extensions.Hosting).</Description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand Down Expand Up @@ -124,7 +124,7 @@ public void AllowNoThrowBehaviorOnUnexpectedOptionWhenHasSubcommand()
}

[Fact]
public void ItDoesNotInitalizeClassUnlessNecessary()
public void ItDoesNotInitializeClassUnlessNecessary()
{
using var app = new CommandLineApplication<ThrowsInCtorClass>(new TestConsole(_output));
app.Conventions.UseDefaultConventions();
Expand All @@ -139,7 +139,7 @@ class SimpleCommand
}

[Fact]
public void ItDoesNotInitalizeParentClassUnlessNecessary()
public void ItDoesNotInitializeParentClassUnlessNecessary()
{
using var app = new CommandLineApplication<ThrowsInCtorClass>(new TestConsole(_output));
app.Conventions.UseDefaultConventions();
Expand Down
48 changes: 48 additions & 0 deletions test/CommandLineUtils.Tests/ExecuteMethodConventionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ProgramWithExecuteAndKeyedArgumentInjection>();

app.AdditionalServices = serviceCollection.BuildServiceProvider();

app.Conventions.UseOnExecuteMethodFromModel();
var result = app.Execute();
Assert.Equal(42, result);
}


}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<!-- Once xunit supports nullable reference types, I might reconsider -->
<Nullable>annotations</Nullable>
</PropertyGroup>
Expand All @@ -13,7 +13,9 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.1.1" />
<PackageReference Include="McMaster.Extensions.Xunit" Version="0.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Moq" Version="4.16.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down