diff --git a/src/ConfigurationProcessor.Core/IConfigurationProcessor.cs b/src/ConfigurationProcessor.Core/IConfigurationProcessor.cs new file mode 100644 index 0000000..64a03c9 --- /dev/null +++ b/src/ConfigurationProcessor.Core/IConfigurationProcessor.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) almostchristian. All rights reserved. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; + +namespace ConfigurationProcessor +{ + /// + /// Represents a configuration helper for dynamically executing methods. + /// + public interface IConfigurationProcessor + { + /// + /// Invokes a specificed configuration method with specificed arguments. Method is chosen based on the argument types. + /// + /// + /// + /// + /// + void Invoke(T instance, string methodName, params object[] arguments) + where T : class; + } +} diff --git a/src/ConfigurationProcessor.Core/Implementation/ConfigurationHelperImplementation.cs b/src/ConfigurationProcessor.Core/Implementation/ConfigurationHelperImplementation.cs new file mode 100644 index 0000000..b80d95e --- /dev/null +++ b/src/ConfigurationProcessor.Core/Implementation/ConfigurationHelperImplementation.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) almostchristian. All rights reserved. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Configuration; + +namespace ConfigurationProcessor.Core.Implementation +{ + internal class ConfigurationHelperImplementation : IConfigurationProcessor + { + private readonly ResolutionContext resolutionContext; + private readonly IConfigurationSection configurationSection; + private readonly ConfigurationReaderOptions? options; + + public ConfigurationHelperImplementation(ResolutionContext resolutionContext, IConfigurationSection configurationSection, ConfigurationReaderOptions? options) + { + this.resolutionContext = resolutionContext; + this.configurationSection = configurationSection; + this.options = options; + } + + public void Invoke(T instance, string methodName, params object[] arguments) + where T : class + { + resolutionContext.CallConfigurationMethod( + typeof(T), + methodName, + configurationSection, + null, + Array.Empty(), + null!, + () => arguments.ToList(), + (arguments, methodInfo) => + { + if (methodInfo.IsStatic) + { + arguments.Insert(0, instance); + methodInfo.Invoke(null, arguments.ToArray()); + } + else + { + methodInfo.Invoke(instance, arguments.ToArray()); + } + }); + } + } +} diff --git a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs index acec799..e02b31d 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs @@ -23,10 +23,11 @@ public void AddServices(TConfig builder, string? sectionName, bool getChildren, var builderDirective = string.IsNullOrEmpty(sectionName) ? ConfigurationSection : ConfigurationSection.GetSection(sectionName); if (!getChildren || builderDirective.GetChildren().Any()) { - var methodCalls = ResolutionContext.GetMethodCalls(builderDirective, getChildren); ResolutionContext.CallConfigurationMethods( typeof(TConfig), - methodCalls, + builderDirective, + getChildren, + null, options.MethodFilterFactory, (arguments, methodInfo) => { diff --git a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs index 66a116a..4606e6a 100644 --- a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs @@ -25,191 +25,229 @@ internal static class Extensions private const char GenericTypeMarker = '`'; private const char GenericTypeParameterSeparator = '|'; - public static void CallConfigurationMethods( - this ResolutionContext resolutionContext, - Type extensionArgumentType, - ILookup methods, - MethodFilterFactory? methodFilterFactory, - Action, MethodInfo> invoker) + public static void CallConfigurationMethod( + this ResolutionContext resolutionContext, + Type extensionArgumentType, + string methodName, + IConfigurationSection configSection, + MethodFilterFactory? methodFilterFactory, + TypeResolver[] typeArgs, + Dictionary? paramArgs, + Func>? argumentFactory, + Action, MethodInfo> invoker) { - foreach (var (methodName, (typeArgs, configSection, configArgs)) in methods.SelectMany(g => g.Select(x => (MethodName: g.Key, Config: x)))) + methodFilterFactory ??= MethodFilterFactories.DefaultMethodFilterFactory; + + var (methodFilter, candidateNames) = methodFilterFactory(methodName); + IEnumerable configurationMethods = resolutionContext + .FindConfigurationExtensionMethods(methodName, extensionArgumentType, typeArgs, candidateNames, methodFilter); + configurationMethods = configurationMethods.Union(resolutionContext.AdditionalMethods.Where(m => candidateNames.Contains(m.Name) && methodFilter(m, methodName))).ToList(); + + var suppliedArgumentNames = paramArgs?.Keys.ToArray() ?? Array.Empty(); + + var isCollection = suppliedArgumentNames.IsArray(); + MethodInfo? configurationMethod; + + var args = argumentFactory?.Invoke(); + + if (isCollection) { - var paramArgs = configArgs; - methodFilterFactory ??= MethodFilterFactories.DefaultMethodFilterFactory; + configurationMethod = configurationMethods + .Where(m => + { + var parameters = m.GetParameters(); + if (parameters.Length != (m.IsStatic ? 2 : 1)) + { + return false; + } - var (methodFilter, candidateNames) = methodFilterFactory(methodName); - IEnumerable configurationMethods = resolutionContext - .FindConfigurationExtensionMethods(methodName, extensionArgumentType, typeArgs, candidateNames, methodFilter); - configurationMethods = configurationMethods.Union(resolutionContext.AdditionalMethods.Where(m => candidateNames.Contains(m.Name) && methodFilter(m, methodName))).ToList(); - var suppliedArgumentNames = paramArgs.Keys; + var paramType = parameters[m.IsStatic ? 1 : 0].ParameterType; + var isCollection = paramType.IsArray || (paramType.IsGenericType && typeof(List<>) == paramType.GetGenericTypeDefinition()); - var isCollection = suppliedArgumentNames.IsArray(); - MethodInfo? configurationMethod; - if (isCollection) - { - configurationMethod = configurationMethods - .Where(m => + if (isCollection) { - var parameters = m.GetParameters(); - if (parameters.Length != (m.IsStatic ? 2 : 1)) +#pragma warning disable CA1031 // Do not catch general exception types + try + { +#pragma warning disable S1481 // Unused local variables should be removed + var collection = GetCollection(resolutionContext, configSection!, m); +#pragma warning restore S1481 // Unused local variables should be removed + } + catch { return false; } +#pragma warning restore CA1031 // Do not catch general exception types + } - var paramType = parameters[m.IsStatic ? 1 : 0].ParameterType; - var isCollection = paramType.IsArray || (paramType.IsGenericType && typeof(List<>) == paramType.GetGenericTypeDefinition()); + return isCollection; + }) + .SingleOrDefault($"Ambiguous match while searching for a method that accepts a list or array."); + } + else + { + // for single property, choose the best configuration method by attempting to convert the parameter value + if (suppliedArgumentNames.Length == 1 && string.IsNullOrEmpty(suppliedArgumentNames.Single()) && configSection.Value != null) + { + var argvalue = configSection.Value; + configurationMethod = configurationMethods + .Where(m => + { + var parameters = m.GetParameters(); + System.Diagnostics.Debug.WriteLine(parameters.Count(p => !p.HasDefaultValue)); + if (parameters.Count(p => !p.HasDefaultValue && !p.HasImplicitValueWhenNotSpecified()) != (m.IsStatic ? 2 : 1)) + { + return false; + } + + var paramType = m.GetParameters().Where(p => !p.HasImplicitValueWhenNotSpecified()).ElementAt(m.IsStatic ? 1 : 0).ParameterType; + var isCollection = paramType.IsArray || (paramType.IsGenericType && typeof(List<>) == paramType.GetGenericTypeDefinition()); + if (isCollection) + { + return false; + } - if (isCollection) - { #pragma warning disable CA1031 // Do not catch general exception types - try - { -#pragma warning disable S1481 // Unused local variables should be removed - var collection = GetCollection(m); -#pragma warning restore S1481 // Unused local variables should be removed - } - catch - { - return false; - } + try + { + var argValue = new StringArgumentValue(configSection, argvalue); + argValue.ConvertTo(m, paramType, resolutionContext); + + return true; + } + catch + { + return false; + } #pragma warning restore CA1031 // Do not catch general exception types - } + }) + .SingleOrDefault($"Ambiguous match while searching for a method that accepts a single value.") ?? - return isCollection; - }) - .SingleOrDefault($"Ambiguous match while searching for a method that accepts a list or array."); + // if no match found, choose the parameterless overload + configurationMethods.SingleOrDefault(m => m.GetParameters().Count(p => !p.HasDefaultValue) == (m.IsStatic ? 1 : 0)); + } + else if (args != null) + { + configurationMethod = configurationMethods.SelectConfigurationMethod(args.Select(a => a.GetType())); } else { - // for single property, choose the best configuration method by attempting to convert the parameter value - if (suppliedArgumentNames.Count == 1 && string.IsNullOrEmpty(suppliedArgumentNames.Single()) && configSection.Value != null) - { - var argvalue = configSection.Value; - configurationMethod = configurationMethods - .Where(m => - { - var parameters = m.GetParameters(); - System.Diagnostics.Debug.WriteLine(parameters.Count(p => !p.HasDefaultValue)); - if (parameters.Count(p => !p.HasDefaultValue) != (m.IsStatic ? 2 : 1)) - { - return false; - } - - var paramType = m.GetParameters().ElementAt(m.IsStatic ? 1 : 0).ParameterType; - var isCollection = paramType.IsArray || (paramType.IsGenericType && typeof(List<>) == paramType.GetGenericTypeDefinition()); - if (isCollection) - { - return false; - } + configurationMethod = configurationMethods.SelectConfigurationMethod(suppliedArgumentNames); + } -#pragma warning disable CA1031 // Do not catch general exception types - try - { - var argValue = new StringArgumentValue(configSection, argvalue); - argValue.ConvertTo(m, paramType, resolutionContext); - - return true; - } - catch - { - return false; - } -#pragma warning restore CA1031 // Do not catch general exception types - }) - .SingleOrDefault($"Ambiguous match while searching for a method that accepts a single value.") ?? + if (configurationMethod == null) + { + // if the method could still not be found, look method that accepts a single dictionary + configurationMethod = configurationMethods + .Where(m => + { + var parameters = m.GetParameters(); + if (parameters.Length != (m.IsStatic ? 2 : 1)) + { + return false; + } - // if no match found, choose the parameterless overload - configurationMethods.SingleOrDefault(m => m.GetParameters().Count(p => !p.HasDefaultValue) == (m.IsStatic ? 1 : 0)); - } - else - { - configurationMethod = configurationMethods.SelectConfigurationMethod(suppliedArgumentNames); - } + var paramType = parameters[m.IsStatic ? 1 : 0].ParameterType; + return paramType.IsGenericType && typeof(Dictionary<,>) == paramType.GetGenericTypeDefinition(); + }) + .SingleOrDefault($"Ambigous match while searching for a method that accepts Dictionary<,>."); - if (configurationMethod == null) + if (configurationMethod != null) { - // if the method could still not be found, look method that accepts a single dictionary - configurationMethod = configurationMethods - .Where(m => - { - var parameters = m.GetParameters(); - if (parameters.Length != (m.IsStatic ? 2 : 1)) - { - return false; - } - - var paramType = parameters[m.IsStatic ? 1 : 0].ParameterType; - return paramType.IsGenericType && typeof(Dictionary<,>) == paramType.GetGenericTypeDefinition(); - }) - .SingleOrDefault($"Ambigous match while searching for a method that accepts Dictionary<,>."); - - if (configurationMethod != null) - { - paramArgs = new Dictionary + paramArgs = new Dictionary { { string.Empty, (new ObjectArgumentValue(configSection), configSection) }, }; - } } } + } - IEnumerable? GetCollection(MethodInfo method) + if (configurationMethod != null) + { + if (argumentFactory != null && args != null) { - var argValue = new ObjectArgumentValue(configSection!); - var collectionType = method.GetParameters().ElementAt(method.IsStatic ? 1 : 0).ParameterType; - return argValue.ConvertTo(method, collectionType, resolutionContext) as ICollection; + invoker(args, configurationMethod); } - - if (configurationMethod != null) + else if (isCollection) { - if (isCollection) - { - var collection = GetCollection(configurationMethod); - invoker(new List { collection! }, configurationMethod); - } - else - { - var parameters = configurationMethod.GetParameters().Skip(configurationMethod.IsStatic ? 1 : 0).ToArray(); - List args = new List(); - - for (int i = 0; i < parameters.Length; i++) - { - var p = parameters[i]; - var directive = paramArgs.FirstOrDefault(s => string.IsNullOrEmpty(s.Key) || ParameterNameMatches(p.Name!, s.Key)); - var arg = (directive.Key == null || (string.IsNullOrEmpty(directive.Key) && i > 0)) ? - resolutionContext.GetImplicitValueForNotSpecifiedKey(p, configurationMethod, paramArgs.FirstOrDefault().Value.ConfigSection, methodName)! : - directive.Value.ArgName.ConvertTo(configurationMethod, p.ParameterType, resolutionContext)!; - args.Add(arg); - } - - invoker(args, configurationMethod); - } + var collection = GetCollection(resolutionContext, configSection!, configurationMethod); + invoker(new List { collection! }, configurationMethod); } else { - if (!configurationMethods.Any()) + var parameters = configurationMethod.GetParameters().Skip(configurationMethod.IsStatic ? 1 : 0).ToArray(); + args = new List(); + + for (int i = 0; i < parameters.Length; i++) { - configurationMethods = resolutionContext - .FindConfigurationExtensionMethods(methodName, extensionArgumentType, typeArgs, null, methodFilter) - .Distinct(); + var p = parameters[i]; + var directive = paramArgs.FirstOrDefault(s => string.IsNullOrEmpty(s.Key) || ParameterNameMatches(p.Name!, s.Key)); + var arg = (directive.Key == null || (string.IsNullOrEmpty(directive.Key) && i > 0)) ? + resolutionContext.GetImplicitValueForNotSpecifiedKey(p, configurationMethod, paramArgs.FirstOrDefault().Value.ConfigSection, methodName)! : + directive.Value.ArgName.ConvertTo(configurationMethod, p.ParameterType, resolutionContext)!; + args.Add(arg); } - var errorEventArgs = new ExtensionMethodNotFoundEventArgs( - configurationMethods, - candidateNames, - extensionArgumentType, - paramArgs.ToDictionary(x => x.Key, x => x.Value.ConfigSection)); + invoker(args, configurationMethod); + } + } + else + { + if (!configurationMethods.Any()) + { + configurationMethods = resolutionContext + .FindConfigurationExtensionMethods(methodName, extensionArgumentType, typeArgs, null, methodFilter) + .Distinct(); + } + + var errorEventArgs = new ExtensionMethodNotFoundEventArgs( + configurationMethods, + candidateNames, + extensionArgumentType, + paramArgs.ToDictionary(x => x.Key, x => x.Value.ConfigSection)); - resolutionContext.OnExtensionMethodNotFound(errorEventArgs); + resolutionContext.OnExtensionMethodNotFound(errorEventArgs); - if (!errorEventArgs.Handled) - { - ThrowMissingMethodException(errorEventArgs); - } + if (!errorEventArgs.Handled) + { + ThrowMissingMethodException(errorEventArgs); } } } + public static void CallConfigurationMethods( + this ResolutionContext resolutionContext, + Type extensionArgumentType, + IConfigurationSection directive, + bool getChildren, + IEnumerable? exclude, + MethodFilterFactory? methodFilterFactory, + Action, MethodInfo> invoker) + { + var methods = resolutionContext.GetMethodCalls(directive, getChildren, exclude); + foreach (var (methodName, (typeArgs, configSection, configArgs)) in methods.SelectMany(g => g.Select(x => (MethodName: g.Key, Config: x)))) + { + var paramArgs = configArgs; + CallConfigurationMethod( + resolutionContext, + extensionArgumentType, + methodName, + configSection, + methodFilterFactory, + typeArgs, + paramArgs, + null, + invoker); + } + } + + private static IEnumerable? GetCollection(ResolutionContext resolutionContext, IConfigurationSection configSection, MethodInfo method) + { + var argValue = new ObjectArgumentValue(configSection); + var collectionType = method.GetParameters().ElementAt(method.IsStatic ? 1 : 0).ParameterType; + return argValue.ConvertTo(method, collectionType, resolutionContext) as ICollection; + } + private static void ThrowMissingMethodException(ExtensionMethodNotFoundEventArgs args) { string message; @@ -391,11 +429,11 @@ public static void BindMappableValues( var excludeKeys = new HashSet(properties.Select(x => x.Name).Union(excludedKeys), StringComparer.OrdinalIgnoreCase); - var methodCalls = resolutionContext.GetMethodCalls(sourceConfigurationSection, true, excludeKeys); - resolutionContext.CallConfigurationMethods( targetType, - methodCalls, + sourceConfigurationSection, + true, + excludeKeys, null, (arguments, methodInfo) => { @@ -498,6 +536,10 @@ private static Delegate GenerateLambda( { return sourceConfigurationSection; } + else if (parameter.ParameterType == typeof(IConfigurationProcessor)) + { + return new ConfigurationHelperImplementation(resolutionContext, sourceConfigurationSection!, null); + } return parameter.DefaultValue; } @@ -615,6 +657,23 @@ private static T SingleOrDefault(this IEnumerable source, FormattableStrin } } + private static MethodInfo? SelectConfigurationMethod( + this IEnumerable candidateMethods, + IEnumerable suppliedArgumentTypes) + { + return candidateMethods + .FirstOrDefault(m => + { + IEnumerable parameters = m.GetParameters(); + if (m.IsStatic) + { + parameters = parameters.Skip(1); + } + + return parameters.Select(x => x.ParameterType).SequenceEqual(suppliedArgumentTypes); + }); + } + private static MethodInfo? SelectConfigurationMethod( this IEnumerable candidateMethods, IEnumerable suppliedArgumentNames) @@ -752,7 +811,8 @@ private static bool HasImplicitValueWhenNotSpecified(this ParameterInfo paramInf // parameters of type IConfiguration are implicitly populated with provided Configuration || paramInfo.ParameterType == typeof(IConfiguration) - || paramInfo.ParameterType == typeof(IConfigurationSection); + || paramInfo.ParameterType == typeof(IConfigurationSection) + || paramInfo.ParameterType == typeof(IConfigurationProcessor); } private static bool IsConfigurationOptionsBuilder(this ParameterInfo paramInfo, [NotNullWhen(true)] out Type? argumentType) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fcecc10..9cbbaa7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ - 1.5.1 + 1.6.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.6.0 + - Added IConfigurationProcessor interface to dynamically call methods v1.5.1 - Fixed exception with optional parameters v1.5.0 diff --git a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs index 041b7c0..392bbf3 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs +++ b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs @@ -1283,6 +1283,42 @@ public void WithObjectNotation_CallExtensionMethodOnConfigurationObject_Executes Assert.Equal("helloworld2314", option.Value.Name); } + [Fact] + public void WithObjectNotation_UsingHelper_CallsDynamicAddConfigureName() + { + var randomValue = Guid.NewGuid().ToString(); + var json = @$" +{{ + 'WithHelper': '{randomValue}' +}}"; + + var sp = BuildFromJson(json); + var option = sp.GetService>(); + + Assert.NotNull(option); + Assert.Equal(randomValue, option.Value.Name); + } + + [Theory] + [InlineData("Time")] + [InlineData("Time2")] + public void WithObjectNotation_UsingHelperWithComplexOptions_CallsDynamicAddConfigureName(string timeProperty) + { + var randomValue = Guid.NewGuid().ToString(); + var json = @$" +{{ + 'WithHelper': {{ + '{timeProperty}' : '13:00:10' + }} +}}"; + + var sp = BuildFromJson(json); + var option = sp.GetService>(); + + Assert.NotNull(option); + Assert.Equal(new TimeSpan(13, 0, 10), option.Value.Value.Time); + } + private IServiceProvider BuildFromJson(string json) { var serviceCollection = ProcessJson(json); diff --git a/tests/TestDummies/DummyServiceCollectionExtensions.cs b/tests/TestDummies/DummyServiceCollectionExtensions.cs index 7a0c1d2..b081944 100644 --- a/tests/TestDummies/DummyServiceCollectionExtensions.cs +++ b/tests/TestDummies/DummyServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Copyright (c) Integrated Health Information Systems Pte Ltd. All rights reserved. // ------------------------------------------------------------------------------------------------- +using ConfigurationProcessor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; @@ -275,5 +276,17 @@ public static void Append(this ComplexObject obj, string value) { obj.Name += value; } + + public static IServiceCollection AddWithHelper(this IServiceCollection services, string text, IConfigurationProcessor helper) + { + services.AddConfigurationAction(c => helper.Invoke(c, "AddConfigureName", text)); + return services; + } + + public static IServiceCollection AddWithHelper(this IServiceCollection services, Action configure, IConfigurationProcessor helper) + { + services.AddConfigurationAction(c => helper.Invoke(c, "AddConfigureValue", configure)); + return services; + } } } diff --git a/tests/TestDummies/TestDummies.csproj b/tests/TestDummies/TestDummies.csproj index 06b680c..680373b 100644 --- a/tests/TestDummies/TestDummies.csproj +++ b/tests/TestDummies/TestDummies.csproj @@ -12,4 +12,8 @@ + + + +