diff --git a/README.md b/README.md index 712c5d3..142763d 100644 --- a/README.md +++ b/README.md @@ -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");` diff --git a/sample/SampleOutboxApi/Extensions.cs b/sample/SampleOutboxApi/Extensions.cs index 9f30457..d388e69 100644 --- a/sample/SampleOutboxApi/Extensions.cs +++ b/sample/SampleOutboxApi/Extensions.cs @@ -14,15 +14,6 @@ public static TracerProviderBuilder SetDefaultResourceBuilder(this TracerProvide return builder.SetResourceBuilder(resourceBuilder); } - public static IBusRegistrationConfigurator UsingActiveMq(this IBusRegistrationConfigurator configurator, Action 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); diff --git a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs index 9e6728d..697e407 100644 --- a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs @@ -19,7 +19,7 @@ namespace ConfigurationProcessor.Core.Implementation { internal static class Extensions { - public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo(o => BindMappableValues(default!, default!, default!, default!, default!, default!)); + public static readonly MethodInfo BindMappableValuesMethod = ReflectionUtil.GetMethodInfo(o => BindMappableValues(default!, default!, default!, default!, default!, default!)).MakeGenericMethod(typeof(object)); private const string GenericTypePattern = "(?[a-zA-Z][a-zA-Z0-9\\.]+)<(?.+)>"; private static readonly Regex GenericTypeRegex = new Regex(GenericTypePattern, RegexOptions.Compiled); private const char GenericTypeMarker = '`'; @@ -423,9 +423,9 @@ private static TypeResolver ReadGenericType(this ResolutionContext resolutionCon } } - public static void BindMappableValues( + public static void BindMappableValues( this ResolutionContext resolutionContext, - object target, + T target, Type targetType, MethodInfo configurationMethod, IConfigurationSection sourceConfigurationSection, @@ -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 @@ -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(); + 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 { originalKey } : new List(); - 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(); + + 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 { originalKey } : new List(); + 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> 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> 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(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( @@ -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 suppliedNames) diff --git a/src/ConfigurationProcessor.Core/Implementation/ReflectionUtil.cs b/src/ConfigurationProcessor.Core/Implementation/ReflectionUtil.cs index a3e72d1..70e6e6c 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ReflectionUtil.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ReflectionUtil.cs @@ -32,6 +32,9 @@ private static ModuleBuilder InitializeModuleBuilder() public static MethodInfo GetMethodInfo(Expression> methodCallExpression) => GetMethodInfo((LambdaExpression)methodCallExpression); + public static MethodInfo GetGenericMethodInfo(Expression methodCallExpression) + => GetMethodInfo(methodCallExpression); + private static MethodInfo GetMethodInfo(LambdaExpression methodCallExpr) { var baseExpression = methodCallExpr.Body; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e3e64bd..3518726 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ - 1.7.2 + 1.8.0 $(Version).$([System.DateTime]::Now.ToString(yy))$([System.DateTime]::Now.DayOfYear.ToString(000)) $(Version) $(FileVersion)-$(GIT_VERSION) @@ -23,6 +23,8 @@ dependencyinjection;configuration;ioc;di; README.md +v1.8.0 + - Support Action<T1, T2> and Action<T1, T2, T3> as configuration targets. v1.7.2 - Added special handling for retrieving ConnectionStrings - Lambda parameters can now be in any position diff --git a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs index 7ffce29..83e390a 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs +++ b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs @@ -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>(); + Assert.Equal("abcdef", option1.Value.Name); + Assert.Equal(new TimeSpan(13, 0, 10), option1.Value.Value.Time); + var option2 = sp.GetService>(); + 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>(); + Assert.Equal("abcdef", option1.Value.Name); + Assert.False(option1.Value.Value.Time.HasValue); + var option2 = sp.GetService>(); + Assert.Equal("abcdef", option2.Value.ConnectionString); + } + private IServiceProvider BuildFromJson(string json) { var serviceCollection = ProcessJson(json); diff --git a/tests/TestDummies/DummyServiceCollectionExtensions.cs b/tests/TestDummies/DummyServiceCollectionExtensions.cs index 0eb2ff2..a59de6e 100644 --- a/tests/TestDummies/DummyServiceCollectionExtensions.cs +++ b/tests/TestDummies/DummyServiceCollectionExtensions.cs @@ -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; @@ -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(this ComplexObject obj, string value) @@ -303,5 +307,37 @@ public static void ConnectionString(this ComplexObject obj, string value) { obj.Name = value; } + + public static IServiceCollection MultiParameterDelegate2(this IServiceCollection services, Action 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 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; + } } }