Skip to content

Commit

Permalink
Support multi argument delegates
Browse files Browse the repository at this point in the history
  • Loading branch information
almostchristian committed Nov 12, 2022
1 parent 8efb3a8 commit 9961d1e
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Given the configuration below:
```

```csharp
public stastic IServiceCollection AddDbConnection(this IServiceCollection services, string connectionString)
public static IServiceCollection AddDbConnection(this IServiceCollection services, string connectionString)
```

The configuration above is equivalent to calling `services.AddDbConnection("abcd");`
Expand Down
9 changes: 0 additions & 9 deletions sample/SampleOutboxApi/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,6 @@ public static TracerProviderBuilder SetDefaultResourceBuilder(this TracerProvide
return builder.SetResourceBuilder(resourceBuilder);
}

public static IBusRegistrationConfigurator UsingActiveMq(this IBusRegistrationConfigurator configurator, Action<IActiveMqBusFactoryConfigurator> configure)
{
configurator.UsingActiveMq((ctx, cfg) =>
{
configure?.Invoke(cfg);
});
return configurator;
}

public static IActiveMqBusFactoryConfigurator ConnectionString(this IActiveMqBusFactoryConfigurator configurator, string value, IConfigurationProcessor configurationProcessor)
{
var uri = new Uri(configurationProcessor.RootConfiguration.GetConnectionString(value) ?? value);
Expand Down
147 changes: 102 additions & 45 deletions src/ConfigurationProcessor.Core/Implementation/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace ConfigurationProcessor.Core.Implementation
{
internal static class Extensions
{
public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo<object>(o => BindMappableValues(default!, default!, default!, default!, default!, default!));
public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo<object>(o => BindMappableValues<object>(default!, default!, default!, default!, default!, default!)).MakeGenericMethod(typeof(object));
private const string GenericTypePattern = "(?<typename>[a-zA-Z][a-zA-Z0-9\\.]+)<(?<genparam>.+)>";
private static readonly Regex GenericTypeRegex = new Regex(GenericTypePattern, RegexOptions.Compiled);
private const char GenericTypeMarker = '`';
Expand Down Expand Up @@ -423,9 +423,9 @@ private static TypeResolver ReadGenericType(this ResolutionContext resolutionCon
}
}

public static void BindMappableValues(
public static void BindMappableValues<T>(
this ResolutionContext resolutionContext,
object target,
T target,
Type targetType,
MethodInfo configurationMethod,
IConfigurationSection sourceConfigurationSection,
Expand Down Expand Up @@ -455,7 +455,7 @@ public static void BindMappableValues(
if (methodInfo.IsStatic)
{
var nargs = arguments.ToList();
nargs.Insert(0, target);
nargs.Insert(0, target!);
methodInfo.Invoke(null, nargs.ToArray());
}
else
Expand All @@ -472,45 +472,98 @@ internal static Delegate GenerateLambda(
Type argumentType,
string? originalKey)
{
var typeParameter = Expression.Parameter(argumentType);
Expression bodyExpression;
if (sourceConfigurationSection?.Exists() == true)
if (argumentType.Name.StartsWith("Action`2", StringComparison.Ordinal))
{
var genArgs = argumentType.GetGenericArguments();
var (parameterExpression1, bodyExpression1) = BuildExpressionWithParam(genArgs[0], true);
var (parameterExpression2, bodyExpression2) = BuildExpressionWithParam(genArgs[1], true);
var combinedArgType = typeof(ValueTuple<,>).MakeGenericType(genArgs);
var combinedArg = Expression.New(combinedArgType.GetConstructor(genArgs), parameterExpression1, parameterExpression2);
var bodyExpression3 = BuildExpression(combinedArg, combinedArgType, true);
var combinedBody = Expression.Block(bodyExpression1, bodyExpression2, bodyExpression3);
var lambda = Expression.Lambda(argumentType, combinedBody, parameterExpression1, parameterExpression2).Compile();
return lambda;
}
else if (argumentType.Name.StartsWith("Action`3", StringComparison.Ordinal))
{
var genArgs = argumentType.GetGenericArguments();
var (parameterExpression1, bodyExpression1) = BuildExpressionWithParam(genArgs[0], true);
var (parameterExpression2, bodyExpression2) = BuildExpressionWithParam(genArgs[1], true);
var (parameterExpression3, bodyExpression3) = BuildExpressionWithParam(genArgs[2], true);
var combinedArgType = typeof(ValueTuple<,,>).MakeGenericType(genArgs);
var combinedArg = Expression.New(combinedArgType.GetConstructor(genArgs), parameterExpression1, parameterExpression2, parameterExpression3);
var bodyExpression4 = BuildExpression(combinedArg, combinedArgType, true);
var combinedBody = Expression.Block(bodyExpression1, bodyExpression2, bodyExpression3, bodyExpression4);
var lambda = Expression.Lambda(argumentType, combinedBody, parameterExpression1, parameterExpression2, parameterExpression3).Compile();
return lambda;
}
else
{
var methodExpressions = new List<Expression>();
var (parameterExpression, bodyExpression) = BuildExpressionWithParam(argumentType);

var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(argumentType), bodyExpression, parameterExpression).Compile();
return lambda;
}

var childResolutionContext = new ResolutionContext(resolutionContext.AssemblyFinder, resolutionContext.RootConfiguration, sourceConfigurationSection, resolutionContext.AdditionalMethods, resolutionContext.OnExtensionMethodNotFound, argumentType);
(ParameterExpression, Expression) BuildExpressionWithParam(Type argumentType, bool handleMissing = false)
{
var parameterExpression = Expression.Parameter(argumentType);
return (parameterExpression, BuildExpression(parameterExpression, argumentType, handleMissing));
}

var keysToExclude = originalKey != null ? new List<string> { originalKey } : new List<string>();
if (int.TryParse(sourceConfigurationSection.Key, out _))
Expression BuildExpression(Expression parameterExpression, Type argumentType, bool handleMissing = false)
{
Expression bodyExpression;
if (sourceConfigurationSection?.Exists() == true)
{
// integer key indicates that this is from an array
keysToExclude.Add("Name");
}
var methodExpressions = new List<Expression>();

var childResolutionContext = new ResolutionContext(
resolutionContext.AssemblyFinder,
resolutionContext.RootConfiguration,
sourceConfigurationSection,
resolutionContext.AdditionalMethods,
handleMissing ? e => e.Handled = true : resolutionContext.OnExtensionMethodNotFound,
argumentType);

var keysToExclude = originalKey != null ? new List<string> { originalKey } : new List<string>();
if (int.TryParse(sourceConfigurationSection.Key, out _))
{
// integer key indicates that this is from an array
keysToExclude.Add("Name");
}

// we want to return a generic lambda that calls bind c => configuration.Bind(c)
Expression<Action<object>> bindExpression = c => sourceConfigurationSection.Bind(c);
var bindMethodExpression = (MethodCallExpression)bindExpression.Body;
methodExpressions.Add(Expression.Call(bindMethodExpression.Method, bindMethodExpression.Arguments[0], typeParameter));
// we want to return a generic lambda that calls bind c => configuration.Bind(c)
if (!parameterExpression.Type.IsValueType)
{
Expression<Action<object>> bindExpression = c => sourceConfigurationSection.Bind(c);
var bindMethodExpression = (MethodCallExpression)bindExpression.Body;
methodExpressions.Add(Expression.Call(bindMethodExpression.Method, bindMethodExpression.Arguments[0], parameterExpression));
}

methodExpressions.Add(
Expression.Call(
BindMappableValuesMethod,
Expression.Constant(childResolutionContext),
typeParameter,
Expression.Constant(argumentType),
Expression.Constant(configurationMethod),
Expression.Constant(sourceConfigurationSection),
Expression.Constant(keysToExclude.ToArray())));
var bindMethod = !parameterExpression.Type.IsValueType ?
BindMappableValuesMethod :
ReflectionUtil.GetGenericMethodInfo(() => BindMappableValues<object>(default!, default!, default!, default!, default!, default!)).MakeGenericMethod(parameterExpression.Type);

methodExpressions.Add(
Expression.Call(
bindMethod,
Expression.Constant(childResolutionContext),
parameterExpression,
Expression.Constant(argumentType),
Expression.Constant(configurationMethod),
Expression.Constant(sourceConfigurationSection),
Expression.Constant(keysToExclude.ToArray())));

bodyExpression = Expression.Block(methodExpressions);
}
else
{
bodyExpression = Expression.Empty();
}

bodyExpression = Expression.Block(methodExpressions);
return bodyExpression;
}
else
{
bodyExpression = Expression.Empty();
}

var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(argumentType), bodyExpression, typeParameter).Compile();
return lambda;
}

private static object? GetImplicitValueForNotSpecifiedKey(
Expand Down Expand Up @@ -845,18 +898,22 @@ private static bool IsConfigurationOptionsBuilder(this ParameterInfo paramInfo,

internal static bool IsConfigurationOptionsBuilder(this Type type, [NotNullWhen(true)] out Type? argumentType)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Action<>))
{
argumentType = type.GenericTypeArguments[0];

// we only accept class types that contain a parameterless public constructor
return true;
}
else
if (type.IsGenericType)
{
argumentType = null;
return false;
if (type.GetGenericTypeDefinition() == typeof(Action<>))
{
argumentType = type.GenericTypeArguments[0];
return true;
}
else if (type.GetGenericTypeDefinition() == typeof(Action<,>) || type.GetGenericTypeDefinition() == typeof(Action<,,>))
{
argumentType = type;
return true;
}
}

argumentType = null;
return false;
}

private static bool ParameterTypeHasPropertyMatches(this Type parameterType, IEnumerable<string> suppliedNames)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ private static ModuleBuilder InitializeModuleBuilder()
public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> methodCallExpression)
=> GetMethodInfo((LambdaExpression)methodCallExpression);

public static MethodInfo GetGenericMethodInfo(Expression<Action> methodCallExpression)
=> GetMethodInfo(methodCallExpression);

private static MethodInfo GetMethodInfo(LambdaExpression methodCallExpr)
{
var baseExpression = methodCallExpr.Body;
Expand Down
4 changes: 3 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.7.2</Version>
<Version>1.8.0</Version>
<FileVersion>$(Version).$([System.DateTime]::Now.ToString(yy))$([System.DateTime]::Now.DayOfYear.ToString(000))</FileVersion>
<PackageVersion>$(Version)</PackageVersion>
<InformationalVersion>$(FileVersion)-$(GIT_VERSION)</InformationalVersion>
Expand All @@ -23,6 +23,8 @@
<PackageTags>dependencyinjection;configuration;ioc;di;</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
v1.8.0
- Support Action&lt;T1, T2&gt; and Action&lt;T1, T2, T3&gt; as configuration targets.
v1.7.2
- Added special handling for retrieving ConnectionStrings
- Lambda parameters can now be in any position
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,49 @@ public void WithObjectNotation_CallExtensionForConnectionString_SetsConnectionSt
Assert.Equal(expectedConnectionStringValue, option.Value.Name);
}

[Fact]
public void WithObjectNotiation_MultiParameterDelegate2_SetsValue()
{
var json = @"
{
'MultiParameterDelegate2': {
'ConnectionString' : 'abc',
'ConfigureValue': {
'Time' : '13:00:10'
},
'MultiConfigure': 'def'
}
}";
var sp = BuildFromJson(json);
var option1 = sp.GetService<IOptions<ComplexObject>>();
Assert.Equal("abcdef", option1.Value.Name);
Assert.Equal(new TimeSpan(13, 0, 10), option1.Value.Value.Time);
var option2 = sp.GetService<IOptions<DbConnection>>();
Assert.Equal("abcdef", option2.Value.ConnectionString);
}

[Fact]
public void WithObjectNotiation_MultiParameterDelegate3_SetsValue()
{
var json = @"
{
'MultiParameterDelegate3': {
'ConnectionString' : 'abc',
'ConfigureValue': {
'Time' : '13:00:10'
},
'MultiConfigure': 'def',
'Reset2': true
}
}";
var sp = BuildFromJson(json);
var option1 = sp.GetService<IOptions<ComplexObject>>();
Assert.Equal("abcdef", option1.Value.Name);
Assert.False(option1.Value.Value.Time.HasValue);
var option2 = sp.GetService<IOptions<DbConnection>>();
Assert.Equal("abcdef", option2.Value.ConnectionString);
}

private IServiceProvider BuildFromJson(string json)
{
var serviceCollection = ProcessJson(json);
Expand Down
38 changes: 37 additions & 1 deletion tests/TestDummies/DummyServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using ConfigurationProcessor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -269,7 +270,10 @@ public static void AddConfigureByInterfaceValue(this IComplexObject configuratio

public static void Reset2(this ComplexObject.ChildValue childValue)
{
childValue.Time = null;
if (childValue != null)
{
childValue.Time = null;
}
}

public static void Append<T>(this ComplexObject obj, string value)
Expand Down Expand Up @@ -303,5 +307,37 @@ public static void ConnectionString(this ComplexObject obj, string value)
{
obj.Name = value;
}

public static IServiceCollection MultiParameterDelegate2(this IServiceCollection services, Action<ComplexObject, DbConnection> configurator)
{
var complexObj = new ComplexObject();
var dbConn = new DbConnection();
configurator(complexObj, dbConn);
services.AddSingleton(Options.Create(complexObj));
services.AddSingleton(Options.Create(dbConn));
return services;
}

public static IServiceCollection MultiParameterDelegate3(this IServiceCollection services, Action<ComplexObject, ComplexObject.ChildValue, DbConnection> configurator)
{
var complexObj = new ComplexObject { Value = new ComplexObject.ChildValue() };
var dbConn = new DbConnection();
configurator(complexObj, complexObj.Value, dbConn);
services.AddSingleton(Options.Create(complexObj));
services.AddSingleton(Options.Create(dbConn));
return services;
}

public static void MultiConfigure(this (ComplexObject Obj, DbConnection Conn) configurator, string value)
{
configurator.Obj.Name += value;
configurator.Conn.ConnectionString += value;
}

public static void MultiConfigure(this (ComplexObject Obj, ComplexObject.ChildValue Child, DbConnection Conn) configurator, string value)
{
configurator.Obj.Name += value;
configurator.Conn.ConnectionString += value;
}
}
}

0 comments on commit 9961d1e

Please sign in to comment.