Skip to content

Commit 9961d1e

Browse files
Support multi argument delegates
1 parent 8efb3a8 commit 9961d1e

File tree

7 files changed

+189
-57
lines changed

7 files changed

+189
-57
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ Given the configuration below:
243243
```
244244

245245
```csharp
246-
public stastic IServiceCollection AddDbConnection(this IServiceCollection services, string connectionString)
246+
public static IServiceCollection AddDbConnection(this IServiceCollection services, string connectionString)
247247
```
248248

249249
The configuration above is equivalent to calling `services.AddDbConnection("abcd");`

sample/SampleOutboxApi/Extensions.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ public static TracerProviderBuilder SetDefaultResourceBuilder(this TracerProvide
1414
return builder.SetResourceBuilder(resourceBuilder);
1515
}
1616

17-
public static IBusRegistrationConfigurator UsingActiveMq(this IBusRegistrationConfigurator configurator, Action<IActiveMqBusFactoryConfigurator> configure)
18-
{
19-
configurator.UsingActiveMq((ctx, cfg) =>
20-
{
21-
configure?.Invoke(cfg);
22-
});
23-
return configurator;
24-
}
25-
2617
public static IActiveMqBusFactoryConfigurator ConnectionString(this IActiveMqBusFactoryConfigurator configurator, string value, IConfigurationProcessor configurationProcessor)
2718
{
2819
var uri = new Uri(configurationProcessor.RootConfiguration.GetConnectionString(value) ?? value);

src/ConfigurationProcessor.Core/Implementation/Extensions.cs

Lines changed: 102 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace ConfigurationProcessor.Core.Implementation
1919
{
2020
internal static class Extensions
2121
{
22-
public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo<object>(o => BindMappableValues(default!, default!, default!, default!, default!, default!));
22+
public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo<object>(o => BindMappableValues<object>(default!, default!, default!, default!, default!, default!)).MakeGenericMethod(typeof(object));
2323
private const string GenericTypePattern = "(?<typename>[a-zA-Z][a-zA-Z0-9\\.]+)<(?<genparam>.+)>";
2424
private static readonly Regex GenericTypeRegex = new Regex(GenericTypePattern, RegexOptions.Compiled);
2525
private const char GenericTypeMarker = '`';
@@ -423,9 +423,9 @@ private static TypeResolver ReadGenericType(this ResolutionContext resolutionCon
423423
}
424424
}
425425

426-
public static void BindMappableValues(
426+
public static void BindMappableValues<T>(
427427
this ResolutionContext resolutionContext,
428-
object target,
428+
T target,
429429
Type targetType,
430430
MethodInfo configurationMethod,
431431
IConfigurationSection sourceConfigurationSection,
@@ -455,7 +455,7 @@ public static void BindMappableValues(
455455
if (methodInfo.IsStatic)
456456
{
457457
var nargs = arguments.ToList();
458-
nargs.Insert(0, target);
458+
nargs.Insert(0, target!);
459459
methodInfo.Invoke(null, nargs.ToArray());
460460
}
461461
else
@@ -472,45 +472,98 @@ internal static Delegate GenerateLambda(
472472
Type argumentType,
473473
string? originalKey)
474474
{
475-
var typeParameter = Expression.Parameter(argumentType);
476-
Expression bodyExpression;
477-
if (sourceConfigurationSection?.Exists() == true)
475+
if (argumentType.Name.StartsWith("Action`2", StringComparison.Ordinal))
476+
{
477+
var genArgs = argumentType.GetGenericArguments();
478+
var (parameterExpression1, bodyExpression1) = BuildExpressionWithParam(genArgs[0], true);
479+
var (parameterExpression2, bodyExpression2) = BuildExpressionWithParam(genArgs[1], true);
480+
var combinedArgType = typeof(ValueTuple<,>).MakeGenericType(genArgs);
481+
var combinedArg = Expression.New(combinedArgType.GetConstructor(genArgs), parameterExpression1, parameterExpression2);
482+
var bodyExpression3 = BuildExpression(combinedArg, combinedArgType, true);
483+
var combinedBody = Expression.Block(bodyExpression1, bodyExpression2, bodyExpression3);
484+
var lambda = Expression.Lambda(argumentType, combinedBody, parameterExpression1, parameterExpression2).Compile();
485+
return lambda;
486+
}
487+
else if (argumentType.Name.StartsWith("Action`3", StringComparison.Ordinal))
488+
{
489+
var genArgs = argumentType.GetGenericArguments();
490+
var (parameterExpression1, bodyExpression1) = BuildExpressionWithParam(genArgs[0], true);
491+
var (parameterExpression2, bodyExpression2) = BuildExpressionWithParam(genArgs[1], true);
492+
var (parameterExpression3, bodyExpression3) = BuildExpressionWithParam(genArgs[2], true);
493+
var combinedArgType = typeof(ValueTuple<,,>).MakeGenericType(genArgs);
494+
var combinedArg = Expression.New(combinedArgType.GetConstructor(genArgs), parameterExpression1, parameterExpression2, parameterExpression3);
495+
var bodyExpression4 = BuildExpression(combinedArg, combinedArgType, true);
496+
var combinedBody = Expression.Block(bodyExpression1, bodyExpression2, bodyExpression3, bodyExpression4);
497+
var lambda = Expression.Lambda(argumentType, combinedBody, parameterExpression1, parameterExpression2, parameterExpression3).Compile();
498+
return lambda;
499+
}
500+
else
478501
{
479-
var methodExpressions = new List<Expression>();
502+
var (parameterExpression, bodyExpression) = BuildExpressionWithParam(argumentType);
503+
504+
var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(argumentType), bodyExpression, parameterExpression).Compile();
505+
return lambda;
506+
}
480507

481-
var childResolutionContext = new ResolutionContext(resolutionContext.AssemblyFinder, resolutionContext.RootConfiguration, sourceConfigurationSection, resolutionContext.AdditionalMethods, resolutionContext.OnExtensionMethodNotFound, argumentType);
508+
(ParameterExpression, Expression) BuildExpressionWithParam(Type argumentType, bool handleMissing = false)
509+
{
510+
var parameterExpression = Expression.Parameter(argumentType);
511+
return (parameterExpression, BuildExpression(parameterExpression, argumentType, handleMissing));
512+
}
482513

483-
var keysToExclude = originalKey != null ? new List<string> { originalKey } : new List<string>();
484-
if (int.TryParse(sourceConfigurationSection.Key, out _))
514+
Expression BuildExpression(Expression parameterExpression, Type argumentType, bool handleMissing = false)
515+
{
516+
Expression bodyExpression;
517+
if (sourceConfigurationSection?.Exists() == true)
485518
{
486-
// integer key indicates that this is from an array
487-
keysToExclude.Add("Name");
488-
}
519+
var methodExpressions = new List<Expression>();
520+
521+
var childResolutionContext = new ResolutionContext(
522+
resolutionContext.AssemblyFinder,
523+
resolutionContext.RootConfiguration,
524+
sourceConfigurationSection,
525+
resolutionContext.AdditionalMethods,
526+
handleMissing ? e => e.Handled = true : resolutionContext.OnExtensionMethodNotFound,
527+
argumentType);
528+
529+
var keysToExclude = originalKey != null ? new List<string> { originalKey } : new List<string>();
530+
if (int.TryParse(sourceConfigurationSection.Key, out _))
531+
{
532+
// integer key indicates that this is from an array
533+
keysToExclude.Add("Name");
534+
}
489535

490-
// we want to return a generic lambda that calls bind c => configuration.Bind(c)
491-
Expression<Action<object>> bindExpression = c => sourceConfigurationSection.Bind(c);
492-
var bindMethodExpression = (MethodCallExpression)bindExpression.Body;
493-
methodExpressions.Add(Expression.Call(bindMethodExpression.Method, bindMethodExpression.Arguments[0], typeParameter));
536+
// we want to return a generic lambda that calls bind c => configuration.Bind(c)
537+
if (!parameterExpression.Type.IsValueType)
538+
{
539+
Expression<Action<object>> bindExpression = c => sourceConfigurationSection.Bind(c);
540+
var bindMethodExpression = (MethodCallExpression)bindExpression.Body;
541+
methodExpressions.Add(Expression.Call(bindMethodExpression.Method, bindMethodExpression.Arguments[0], parameterExpression));
542+
}
494543

495-
methodExpressions.Add(
496-
Expression.Call(
497-
BindMappableValuesMethod,
498-
Expression.Constant(childResolutionContext),
499-
typeParameter,
500-
Expression.Constant(argumentType),
501-
Expression.Constant(configurationMethod),
502-
Expression.Constant(sourceConfigurationSection),
503-
Expression.Constant(keysToExclude.ToArray())));
544+
var bindMethod = !parameterExpression.Type.IsValueType ?
545+
BindMappableValuesMethod :
546+
ReflectionUtil.GetGenericMethodInfo(() => BindMappableValues<object>(default!, default!, default!, default!, default!, default!)).MakeGenericMethod(parameterExpression.Type);
547+
548+
methodExpressions.Add(
549+
Expression.Call(
550+
bindMethod,
551+
Expression.Constant(childResolutionContext),
552+
parameterExpression,
553+
Expression.Constant(argumentType),
554+
Expression.Constant(configurationMethod),
555+
Expression.Constant(sourceConfigurationSection),
556+
Expression.Constant(keysToExclude.ToArray())));
557+
558+
bodyExpression = Expression.Block(methodExpressions);
559+
}
560+
else
561+
{
562+
bodyExpression = Expression.Empty();
563+
}
504564

505-
bodyExpression = Expression.Block(methodExpressions);
565+
return bodyExpression;
506566
}
507-
else
508-
{
509-
bodyExpression = Expression.Empty();
510-
}
511-
512-
var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(argumentType), bodyExpression, typeParameter).Compile();
513-
return lambda;
514567
}
515568

516569
private static object? GetImplicitValueForNotSpecifiedKey(
@@ -845,18 +898,22 @@ private static bool IsConfigurationOptionsBuilder(this ParameterInfo paramInfo,
845898

846899
internal static bool IsConfigurationOptionsBuilder(this Type type, [NotNullWhen(true)] out Type? argumentType)
847900
{
848-
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Action<>))
849-
{
850-
argumentType = type.GenericTypeArguments[0];
851-
852-
// we only accept class types that contain a parameterless public constructor
853-
return true;
854-
}
855-
else
901+
if (type.IsGenericType)
856902
{
857-
argumentType = null;
858-
return false;
903+
if (type.GetGenericTypeDefinition() == typeof(Action<>))
904+
{
905+
argumentType = type.GenericTypeArguments[0];
906+
return true;
907+
}
908+
else if (type.GetGenericTypeDefinition() == typeof(Action<,>) || type.GetGenericTypeDefinition() == typeof(Action<,,>))
909+
{
910+
argumentType = type;
911+
return true;
912+
}
859913
}
914+
915+
argumentType = null;
916+
return false;
860917
}
861918

862919
private static bool ParameterTypeHasPropertyMatches(this Type parameterType, IEnumerable<string> suppliedNames)

src/ConfigurationProcessor.Core/Implementation/ReflectionUtil.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ private static ModuleBuilder InitializeModuleBuilder()
3232
public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> methodCallExpression)
3333
=> GetMethodInfo((LambdaExpression)methodCallExpression);
3434

35+
public static MethodInfo GetGenericMethodInfo(Expression<Action> methodCallExpression)
36+
=> GetMethodInfo(methodCallExpression);
37+
3538
private static MethodInfo GetMethodInfo(LambdaExpression methodCallExpr)
3639
{
3740
var baseExpression = methodCallExpr.Body;

src/Directory.Build.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66

77
<PropertyGroup>
8-
<Version>1.7.2</Version>
8+
<Version>1.8.0</Version>
99
<FileVersion>$(Version).$([System.DateTime]::Now.ToString(yy))$([System.DateTime]::Now.DayOfYear.ToString(000))</FileVersion>
1010
<PackageVersion>$(Version)</PackageVersion>
1111
<InformationalVersion>$(FileVersion)-$(GIT_VERSION)</InformationalVersion>
@@ -23,6 +23,8 @@
2323
<PackageTags>dependencyinjection;configuration;ioc;di;</PackageTags>
2424
<PackageReadmeFile>README.md</PackageReadmeFile>
2525
<PackageReleaseNotes>
26+
v1.8.0
27+
- Support Action&lt;T1, T2&gt; and Action&lt;T1, T2, T3&gt; as configuration targets.
2628
v1.7.2
2729
- Added special handling for retrieving ConnectionStrings
2830
- Lambda parameters can now be in any position

tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,6 +1377,49 @@ public void WithObjectNotation_CallExtensionForConnectionString_SetsConnectionSt
13771377
Assert.Equal(expectedConnectionStringValue, option.Value.Name);
13781378
}
13791379

1380+
[Fact]
1381+
public void WithObjectNotiation_MultiParameterDelegate2_SetsValue()
1382+
{
1383+
var json = @"
1384+
{
1385+
'MultiParameterDelegate2': {
1386+
'ConnectionString' : 'abc',
1387+
'ConfigureValue': {
1388+
'Time' : '13:00:10'
1389+
},
1390+
'MultiConfigure': 'def'
1391+
}
1392+
}";
1393+
var sp = BuildFromJson(json);
1394+
var option1 = sp.GetService<IOptions<ComplexObject>>();
1395+
Assert.Equal("abcdef", option1.Value.Name);
1396+
Assert.Equal(new TimeSpan(13, 0, 10), option1.Value.Value.Time);
1397+
var option2 = sp.GetService<IOptions<DbConnection>>();
1398+
Assert.Equal("abcdef", option2.Value.ConnectionString);
1399+
}
1400+
1401+
[Fact]
1402+
public void WithObjectNotiation_MultiParameterDelegate3_SetsValue()
1403+
{
1404+
var json = @"
1405+
{
1406+
'MultiParameterDelegate3': {
1407+
'ConnectionString' : 'abc',
1408+
'ConfigureValue': {
1409+
'Time' : '13:00:10'
1410+
},
1411+
'MultiConfigure': 'def',
1412+
'Reset2': true
1413+
}
1414+
}";
1415+
var sp = BuildFromJson(json);
1416+
var option1 = sp.GetService<IOptions<ComplexObject>>();
1417+
Assert.Equal("abcdef", option1.Value.Name);
1418+
Assert.False(option1.Value.Value.Time.HasValue);
1419+
var option2 = sp.GetService<IOptions<DbConnection>>();
1420+
Assert.Equal("abcdef", option2.Value.ConnectionString);
1421+
}
1422+
13801423
private IServiceProvider BuildFromJson(string json)
13811424
{
13821425
var serviceCollection = ProcessJson(json);

tests/TestDummies/DummyServiceCollectionExtensions.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using ConfigurationProcessor;
66
using Microsoft.Extensions.Configuration;
77
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Options;
89
using System;
910
using System.Collections.Generic;
1011
using System.Diagnostics.CodeAnalysis;
@@ -269,7 +270,10 @@ public static void AddConfigureByInterfaceValue(this IComplexObject configuratio
269270

270271
public static void Reset2(this ComplexObject.ChildValue childValue)
271272
{
272-
childValue.Time = null;
273+
if (childValue != null)
274+
{
275+
childValue.Time = null;
276+
}
273277
}
274278

275279
public static void Append<T>(this ComplexObject obj, string value)
@@ -303,5 +307,37 @@ public static void ConnectionString(this ComplexObject obj, string value)
303307
{
304308
obj.Name = value;
305309
}
310+
311+
public static IServiceCollection MultiParameterDelegate2(this IServiceCollection services, Action<ComplexObject, DbConnection> configurator)
312+
{
313+
var complexObj = new ComplexObject();
314+
var dbConn = new DbConnection();
315+
configurator(complexObj, dbConn);
316+
services.AddSingleton(Options.Create(complexObj));
317+
services.AddSingleton(Options.Create(dbConn));
318+
return services;
319+
}
320+
321+
public static IServiceCollection MultiParameterDelegate3(this IServiceCollection services, Action<ComplexObject, ComplexObject.ChildValue, DbConnection> configurator)
322+
{
323+
var complexObj = new ComplexObject { Value = new ComplexObject.ChildValue() };
324+
var dbConn = new DbConnection();
325+
configurator(complexObj, complexObj.Value, dbConn);
326+
services.AddSingleton(Options.Create(complexObj));
327+
services.AddSingleton(Options.Create(dbConn));
328+
return services;
329+
}
330+
331+
public static void MultiConfigure(this (ComplexObject Obj, DbConnection Conn) configurator, string value)
332+
{
333+
configurator.Obj.Name += value;
334+
configurator.Conn.ConnectionString += value;
335+
}
336+
337+
public static void MultiConfigure(this (ComplexObject Obj, ComplexObject.ChildValue Child, DbConnection Conn) configurator, string value)
338+
{
339+
configurator.Obj.Name += value;
340+
configurator.Conn.ConnectionString += value;
341+
}
306342
}
307343
}

0 commit comments

Comments
 (0)