diff --git a/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs b/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs index 133cb68..407d344 100644 --- a/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs +++ b/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs @@ -3,6 +3,8 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using ConfigurationProcessor.Core.Assemblies; using ConfigurationProcessor.Core.Implementation; @@ -18,23 +20,23 @@ public static class ConfigurationExtensions /// /// Processes the configuration. /// - /// The object type that is transformed by the configuration. + /// The object type that is transformed by the configuration. /// The configuration object. /// The object that is processed by the configuration. /// The section in the config that will be used in the configuration. /// Additional paths that will be searched. - /// Candidate method name suffixes for matching. - /// Additional methods that can be used for matching. + /// Factory for filtering methods. + /// Additional methods that can be used for matching. /// The object for chaining. /// Thrown when is null. - public static TServices ProcessConfiguration( + public static TContext ProcessConfiguration( this IConfiguration configuration, - TServices context, + TContext context, string configSection, string[]? contextPaths = null, - string[]? candidateMethodNameSuffixes = null, - MethodInfo[]? surrogateMethods = null) - where TServices : class + MethodFilterFactory? methodFilterFactory = null, + MethodInfo[]? additionalMethods = null) + where TContext : class { if (configuration == null) { @@ -45,8 +47,8 @@ public static TServices ProcessConfiguration( configuration, configuration.GetSection(configSection), contextPaths ?? new string?[] { string.Empty }, - candidateMethodNameSuffixes ?? Array.Empty(), - surrogateMethods ?? Array.Empty(), + methodFilterFactory, + additionalMethods ?? Array.Empty(), AssemblyFinder.Auto()); return context; } @@ -56,26 +58,26 @@ internal static TConfig AddFromConfiguration( IConfiguration rootConfiguration, IConfigurationSection configurationSection, string?[] servicePaths, - string[] candidateMethodNameSuffixes, - MethodInfo[] surrogateMethods, + MethodFilterFactory? methodFilterFactory, + MethodInfo[] additionalMethods, AssemblyFinder assemblyFinder) where TConfig : class { - var reader = new ConfigurationReader(configurationSection, assemblyFinder, surrogateMethods, rootConfiguration); + var reader = new ConfigurationReader(configurationSection, assemblyFinder, additionalMethods, rootConfiguration); foreach (var servicePath in servicePaths) { if (string.IsNullOrEmpty(servicePath)) { - reader.AddServices(builder, null, true, candidateMethodNameSuffixes); + reader.AddServices(builder, null, true, methodFilterFactory); } else if (servicePath![0] == '^') { - reader.AddServices(builder, servicePath.Substring(1), false, candidateMethodNameSuffixes); + reader.AddServices(builder, servicePath.Substring(1), false, methodFilterFactory); } else { - reader.AddServices(builder, servicePath, true, candidateMethodNameSuffixes); + reader.AddServices(builder, servicePath, true, methodFilterFactory); } } diff --git a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader.cs b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader.cs index 84a83b1..bac3740 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader.cs @@ -15,20 +15,24 @@ namespace ConfigurationProcessor.Core.Implementation { internal abstract class ConfigurationReader { - private const char GenericTypeMarker = '`'; private readonly IConfigurationSection section; - private readonly MethodInfo[] surrogateMethods; + private readonly MethodInfo[] additionalMethods; private readonly AssemblyFinder assemblyFinder; private readonly ResolutionContext resolutionContext; private readonly IConfiguration rootConfiguration; - protected ConfigurationReader(ResolutionContext resolutionContext, IConfiguration rootConfiguration, AssemblyFinder assemblyFinder, IConfigurationSection configSection, MethodInfo[] surrogateMethods) + protected ConfigurationReader( + ResolutionContext resolutionContext, + IConfiguration rootConfiguration, + AssemblyFinder assemblyFinder, + IConfigurationSection configSection, + MethodInfo[] additionalMethods) { this.resolutionContext = resolutionContext; this.rootConfiguration = rootConfiguration; this.assemblyFinder = assemblyFinder; this.section = configSection; - this.surrogateMethods = surrogateMethods; + this.additionalMethods = additionalMethods; } protected ResolutionContext ResolutionContext => this.resolutionContext; @@ -121,16 +125,18 @@ protected void CallConfigurationMethods( ResolutionContext resolutionContext, Type extensionArgumentType, ILookup methods, - string[] candidateMethodNameSuffixes, + MethodFilterFactory? methodFilterFactory, Action, MethodInfo> invoker) { foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x }))) { var typeArgs = method.Value.Item1; var paramArgs = method.Value.Item3; - var candidateNames = GetCandidateNames(method.Key, candidateMethodNameSuffixes); - List configurationMethods = resolutionContext.FindConfigurationExtensionMethods(extensionArgumentType, typeArgs, candidateNames); - configurationMethods.AddRange(surrogateMethods.Where(m => candidateNames.Contains(m.Name))); + methodFilterFactory ??= MethodFilterFactories.DefaultMethodFilterFactory; + var (methodFilter, candidateNames) = methodFilterFactory(method.Key); + IEnumerable configurationMethods = resolutionContext + .FindConfigurationExtensionMethods(method.Key, extensionArgumentType, typeArgs, candidateNames, methodFilter); + configurationMethods = configurationMethods.Union(additionalMethods.Where(m => methodFilter(m, method.Key))).ToList(); var suppliedArgumentNames = paramArgs.Keys; var isCollection = suppliedArgumentNames.IsArray(); @@ -204,7 +210,6 @@ protected void CallConfigurationMethods( else { var methodsByName = configurationMethods - .Where(m => candidateNames.Contains(m.Name)) .Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Skip(1).Select(p => p.Name))})") .ToList(); @@ -224,30 +229,6 @@ protected void CallConfigurationMethods( } } } - - static List GetCandidateNames(string name, string[] candidateSuffixes) - { - var namesplit = name.Split(GenericTypeMarker); - - var result = new List { name }; - - if (candidateSuffixes.Length > 0) - { - if (namesplit.Length > 1) - { - result.AddRange(candidateSuffixes.Select(x => $"{namesplit[0] + x}{GenericTypeMarker}{namesplit[1]}")); - } - else - { - result.AddRange(candidateSuffixes.Select(x => name + x)); - } - } - - var withPrefix = result.Select(x => "Add" + x).ToList(); - result.AddRange(withPrefix); - - return result; - } } private object? GetImplicitValueForNotSpecifiedKey( @@ -287,7 +268,7 @@ static List GetCandidateNames(string name, string[] candidateSuffixes) var methodCalls = GetMethodCalls(sourceConfigurationSection, true, excludeKeys); - CallConfigurationMethods(currentResolutionContext, argumentType, methodCalls, Array.Empty(), (arguments, methodInfo) => + CallConfigurationMethods(currentResolutionContext, argumentType, methodCalls, null, (arguments, methodInfo) => { var parameters = methodInfo.GetParameters(); diff --git a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs index fe7a5c0..4ba4b14 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs @@ -12,18 +12,18 @@ namespace ConfigurationProcessor.Core.Implementation internal class ConfigurationReader : ConfigurationReader, IConfigurationReader where TConfig : class { - public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, MethodInfo[] surrogateMethods, IConfiguration configuration = null!) - : base(new ResolutionContext(assemblyFinder, configuration!, configSection, typeof(TConfig)), configuration, assemblyFinder, configSection, surrogateMethods) + public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, MethodInfo[] additionalMethods, IConfiguration configuration = null!) + : base(new ResolutionContext(assemblyFinder, configuration!, configSection, typeof(TConfig)), configuration, assemblyFinder, configSection, additionalMethods) { } - public void AddServices(TConfig builder, string? sectionName, bool getChildren, params string[] candidateMethodNameSuffixes) + public void AddServices(TConfig builder, string? sectionName, bool getChildren, MethodFilterFactory? methodFilterFactory) { var builderDirective = string.IsNullOrEmpty(sectionName) ? ConfigurationSection : ConfigurationSection.GetSection(sectionName); if (!getChildren || builderDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(builderDirective, getChildren); - CallConfigurationMethods(ResolutionContext, typeof(TConfig), methodCalls, candidateMethodNameSuffixes, (arguments, methodInfo) => + CallConfigurationMethods(ResolutionContext, typeof(TConfig), methodCalls, methodFilterFactory, (arguments, methodInfo) => { if (methodInfo.IsStatic) { diff --git a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs index 54617d3..2aa3b25 100644 --- a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs @@ -103,9 +103,11 @@ public static bool IsArray(this IEnumerable suppliedArgumentNames) public static List FindConfigurationExtensionMethods( this ResolutionContext resolutionContext, + string key, Type configType, TypeResolver[] typeArgs, - List candidateNames) + IEnumerable candidateNames, + MethodFilter filter) { IReadOnlyCollection configurationAssemblies = resolutionContext.ConfigurationAssemblies; @@ -115,6 +117,7 @@ public static List FindConfigurationExtensionMethods( .Where(t => t.IsSealed && t.IsAbstract && !t.IsNested)) .Union(new[] { configType.GetTypeInfo() }) .SelectMany(t => candidateNames.SelectMany(n => t.GetDeclaredMethods(n))) + .Where(m => filter(m, key)) .Where(m => !m.IsDefined(typeof(CompilerGeneratedAttribute), false) && m.IsPublic && ((m.IsStatic && m.IsDefined(typeof(ExtensionAttribute), false)) || m.DeclaringType == configType)) .Where(m => !m.IsStatic || m.SafeGetParameters().ElementAtOrDefault(0)?.ParameterType.IsAssignableFrom(configType) == true) // If static method, checks that the first parameter is same as the extension type .ToList(); @@ -303,7 +306,7 @@ public static void BindMappableValues( selectedMethods = selectedMethods.Where(m => { var requiredParamCount = m.GetParameters().Count(x => !x.IsOptional); - return requiredParamCount == suppliedArgumentNames.Count() + (m.IsStatic ? 1 : 0); + return requiredParamCount <= suppliedArgumentNames.Count() + (m.IsStatic ? 1 : 0); }); if (selectedMethods.Count() > 1) diff --git a/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs b/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs index 88db6cc..50c4934 100644 --- a/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs +++ b/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs @@ -7,6 +7,6 @@ namespace ConfigurationProcessor.Core.Implementation internal interface IConfigurationReader where TConfig : class { - void AddServices(TConfig builder, string? sectionName, bool getChildren, params string[] candidateMethodNameSuffixes); + void AddServices(TConfig builder, string? sectionName, bool getChildren, MethodFilterFactory methodFilterFactory); } } diff --git a/src/ConfigurationProcessor.Core/MethodFilterFactories.cs b/src/ConfigurationProcessor.Core/MethodFilterFactories.cs new file mode 100644 index 0000000..fddb38d --- /dev/null +++ b/src/ConfigurationProcessor.Core/MethodFilterFactories.cs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) almostchristian. All rights reserved. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ConfigurationProcessor.Core +{ + /// + /// Contains method filter factories. + /// + public static class MethodFilterFactories + { + private static bool DefaultMethodFilter(MethodInfo method, string name) + => true; + + /// + /// Method filter factory that accepts methods with names like '' or 'Add'. + /// + /// The configuration name to search for. + /// The method filter and candidate names. + public static (MethodFilter Filter, IEnumerable CandidateNames) DefaultMethodFilterFactory(string name) + { + var candidates = new HashSet(StringComparer.OrdinalIgnoreCase) { name, $"Add{name}" }; + return (DefaultMethodFilter, candidates); + } + + /// + /// Creates a method filter factory with suffixes. + /// + /// The method name suffixes to search for. + /// The method filter factory. + public static MethodFilterFactory WithSuffixes(params string[] methodNameSuffixes) + => WithSuffixes(DefaultMethodFilter, methodNameSuffixes); + + /// + /// Creates a method filter factory with suffixes. + /// + /// The default method filter factory. + /// The method name suffixes to search for. + /// The method filter factory. + public static MethodFilterFactory WithSuffixes(MethodFilter methodFilter, params string[] methodNameSuffixes) + => WithPrefixAndSuffixes(methodFilter, new[] { "Add" }, methodNameSuffixes); + + /// + /// Creates a method filter factory with prefixes and suffixes. + /// + /// The default method filter factory. + /// The method name suffixes to search for. + /// The method name suffixes to search for. + /// The method filter factory. + public static MethodFilterFactory WithPrefixAndSuffixes(MethodFilter methodFilter, string[] methodNamePrefixes, string[] methodNameSuffixes) + { + return name => + { + var candidates = GetCandidateNames(name, methodNamePrefixes, methodNameSuffixes); + return (methodFilter, candidates); + }; + + static List GetCandidateNames(string name, string[] methodNamePrefixes, string[] candidateSuffixes) + { + const char GenericTypeMarker = '`'; + var namesplit = name.Split(GenericTypeMarker); + + var result = new List { name }; + + if (candidateSuffixes.Length > 0) + { + if (namesplit.Length > 1) + { + result.AddRange(candidateSuffixes.Select(x => $"{namesplit[0] + x}{GenericTypeMarker}{namesplit[1]}")); + } + else + { + result.AddRange(candidateSuffixes.Select(x => name + x)); + } + } + + var withPrefix = result.SelectMany(y => methodNamePrefixes.Select(x => x + y)).ToList(); + result.AddRange(withPrefix); + + return result; + } + } + } +} diff --git a/src/ConfigurationProcessor.Core/MethodFilterFactory.cs b/src/ConfigurationProcessor.Core/MethodFilterFactory.cs new file mode 100644 index 0000000..524d4b1 --- /dev/null +++ b/src/ConfigurationProcessor.Core/MethodFilterFactory.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) almostchristian. All rights reserved. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Reflection; + +namespace ConfigurationProcessor.Core +{ + /// + /// Delegate for filtering a candidate method. + /// + /// The method to evaluate. + /// The configuration name from the method filter factory. + /// True if the method is acceptable. + public delegate bool MethodFilter(MethodInfo methodInfo, string name); + + /// + /// Creates method filters. See for generating factories. + /// + /// The configuration name. + /// Returns the method filter and the candidate names. + public delegate (MethodFilter Filter, IEnumerable CandidateNames) MethodFilterFactory(string name); +} diff --git a/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs b/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs index 5ab946d..b1dc1f8 100644 --- a/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs +++ b/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs @@ -3,7 +3,6 @@ // ------------------------------------------------------------------------------------------------- using System; -using System.Collections.Generic; using System.Reflection; using ConfigurationProcessor.Core; using Microsoft.Extensions.Configuration; @@ -18,23 +17,60 @@ public static class ConfigurationProcessorServiceCollectionExtensions /// /// Adds services from configuration. /// - /// The service collection. + /// The service collection. + /// The configuration to read from. + /// The config section. + /// The service collection for chaining. + /// Thrown when is null. + public static IServiceCollection AddFromConfiguration( + this IServiceCollection services, + IConfiguration configuration, + string servicesSection) + => services.AddFromConfiguration(configuration, servicesSection, null, default(MethodFilterFactory), default); + + /// + /// Adds services from configuration. + /// /// The service collection. /// The configuration to read from. /// The config section. /// Additional service paths. /// Candidate method name suffixes for matching. - /// Additional methods that can be used for matching. + /// Additional methods that can be used for matching. + /// The service collection for chaining. + /// Thrown when is null. + public static IServiceCollection AddFromConfiguration( + this IServiceCollection services, + IConfiguration configuration, + string servicesSection, + string[]? servicePaths, + string[]? candidateMethodNameSuffixes, + MethodInfo[]? additionalMethods = null) + => services.AddFromConfiguration( + configuration, + servicesSection, + servicePaths, + candidateMethodNameSuffixes != null ? MethodFilterFactories.WithSuffixes(candidateMethodNameSuffixes) : null, + additionalMethods); + + /// + /// Adds services from configuration. + /// + /// The service collection. + /// The configuration to read from. + /// The config section. + /// Additional service paths. + /// Factory for creating method filters. + /// Additional methods that can be used for matching. /// The service collection for chaining. /// Thrown when is null. - public static TServices AddFromConfiguration( - this TServices services, + public static IServiceCollection AddFromConfiguration( + this IServiceCollection services, IConfiguration configuration, string servicesSection, - string[]? servicePaths = null, - string[]? candidateMethodNameSuffixes = null, - MethodInfo[]? surrogateMethods = null) - where TServices : class, IEnumerable + string[]? servicePaths, + MethodFilterFactory? methodFilterFactory, + MethodInfo[]? additionalMethods = null) { if (configuration == null) { @@ -45,8 +81,8 @@ public static TServices AddFromConfiguration( services, servicesSection, servicePaths, - candidateMethodNameSuffixes, - surrogateMethods); + methodFilterFactory, + additionalMethods); } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c539f49..51ca338 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ - 0.9.5 + 0.9.6 $(Version).$([System.DateTime]::Now.ToString(yy))$([System.DateTime]::Now.DayOfYear.ToString(000)) $(Version) $(FileVersion)-$(GIT_VERSION)