Skip to content

Commit

Permalink
Added extensible method filtering using MethodFilterFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
Donn Relacion committed Jul 4, 2022
1 parent 36ff958 commit fdfe28a
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 69 deletions.
34 changes: 18 additions & 16 deletions src/ConfigurationProcessor.Core/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ConfigurationProcessor.Core.Assemblies;
using ConfigurationProcessor.Core.Implementation;
Expand All @@ -18,23 +20,23 @@ public static class ConfigurationExtensions
/// <summary>
/// Processes the configuration.
/// </summary>
/// <typeparam name="TServices">The object type that is transformed by the configuration.</typeparam>
/// <typeparam name="TContext">The object type that is transformed by the configuration.</typeparam>
/// <param name="configuration">The configuration object.</param>
/// <param name="context">The object that is processed by the configuration.</param>
/// <param name="configSection">The section in the config that will be used in the configuration.</param>
/// <param name="contextPaths">Additional paths that will be searched.</param>
/// <param name="candidateMethodNameSuffixes">Candidate method name suffixes for matching.</param>
/// <param name="surrogateMethods">Additional methods that can be used for matching.</param>
/// <param name="methodFilterFactory">Factory for filtering methods.</param>
/// <param name="additionalMethods">Additional methods that can be used for matching.</param>
/// <returns>The <paramref name="context"/> object for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configuration"/> is null.</exception>
public static TServices ProcessConfiguration<TServices>(
public static TContext ProcessConfiguration<TContext>(
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)
{
Expand All @@ -45,8 +47,8 @@ public static TServices ProcessConfiguration<TServices>(
configuration,
configuration.GetSection(configSection),
contextPaths ?? new string?[] { string.Empty },
candidateMethodNameSuffixes ?? Array.Empty<string>(),
surrogateMethods ?? Array.Empty<MethodInfo>(),
methodFilterFactory,
additionalMethods ?? Array.Empty<MethodInfo>(),
AssemblyFinder.Auto());
return context;
}
Expand All @@ -56,26 +58,26 @@ internal static TConfig AddFromConfiguration<TConfig>(
IConfiguration rootConfiguration,
IConfigurationSection configurationSection,
string?[] servicePaths,
string[] candidateMethodNameSuffixes,
MethodInfo[] surrogateMethods,
MethodFilterFactory? methodFilterFactory,
MethodInfo[] additionalMethods,
AssemblyFinder assemblyFinder)
where TConfig : class
{
var reader = new ConfigurationReader<TConfig>(configurationSection, assemblyFinder, surrogateMethods, rootConfiguration);
var reader = new ConfigurationReader<TConfig>(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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,16 +125,18 @@ protected void CallConfigurationMethods(
ResolutionContext resolutionContext,
Type extensionArgumentType,
ILookup<string, ConfigLookup> methods,
string[] candidateMethodNameSuffixes,
MethodFilterFactory? methodFilterFactory,
Action<List<object>, 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<MethodInfo> 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<MethodInfo> 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();
Expand Down Expand Up @@ -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();

Expand All @@ -224,30 +229,6 @@ protected void CallConfigurationMethods(
}
}
}

static List<string> GetCandidateNames(string name, string[] candidateSuffixes)
{
var namesplit = name.Split(GenericTypeMarker);

var result = new List<string> { 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(
Expand Down Expand Up @@ -287,7 +268,7 @@ static List<string> GetCandidateNames(string name, string[] candidateSuffixes)

var methodCalls = GetMethodCalls(sourceConfigurationSection, true, excludeKeys);

CallConfigurationMethods(currentResolutionContext, argumentType, methodCalls, Array.Empty<string>(), (arguments, methodInfo) =>
CallConfigurationMethods(currentResolutionContext, argumentType, methodCalls, null, (arguments, methodInfo) =>
{
var parameters = methodInfo.GetParameters();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ namespace ConfigurationProcessor.Core.Implementation
internal class ConfigurationReader<TConfig> : ConfigurationReader, IConfigurationReader<TConfig>
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)
{
Expand Down
7 changes: 5 additions & 2 deletions src/ConfigurationProcessor.Core/Implementation/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ public static bool IsArray(this IEnumerable<string> suppliedArgumentNames)

public static List<MethodInfo> FindConfigurationExtensionMethods(
this ResolutionContext resolutionContext,
string key,
Type configType,
TypeResolver[] typeArgs,
List<string> candidateNames)
IEnumerable<string> candidateNames,
MethodFilter filter)
{
IReadOnlyCollection<Assembly> configurationAssemblies = resolutionContext.ConfigurationAssemblies;

Expand All @@ -115,6 +117,7 @@ public static List<MethodInfo> 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();
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ namespace ConfigurationProcessor.Core.Implementation
internal interface IConfigurationReader<in TConfig>
where TConfig : class
{
void AddServices(TConfig builder, string? sectionName, bool getChildren, params string[] candidateMethodNameSuffixes);
void AddServices(TConfig builder, string? sectionName, bool getChildren, MethodFilterFactory methodFilterFactory);
}
}
89 changes: 89 additions & 0 deletions src/ConfigurationProcessor.Core/MethodFilterFactories.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Contains method filter factories.
/// </summary>
public static class MethodFilterFactories
{
private static bool DefaultMethodFilter(MethodInfo method, string name)
=> true;

/// <summary>
/// Method filter factory that accepts methods with names like '<paramref name="name"/>' or 'Add<paramref name="name"/>'.
/// </summary>
/// <param name="name">The configuration name to search for.</param>
/// <returns>The method filter and candidate names.</returns>
public static (MethodFilter Filter, IEnumerable<string> CandidateNames) DefaultMethodFilterFactory(string name)
{
var candidates = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { name, $"Add{name}" };
return (DefaultMethodFilter, candidates);
}

/// <summary>
/// Creates a method filter factory with suffixes.
/// </summary>
/// <param name="methodNameSuffixes">The method name suffixes to search for.</param>
/// <returns>The method filter factory.</returns>
public static MethodFilterFactory WithSuffixes(params string[] methodNameSuffixes)
=> WithSuffixes(DefaultMethodFilter, methodNameSuffixes);

/// <summary>
/// Creates a method filter factory with suffixes.
/// </summary>
/// <param name="methodFilter">The default method filter factory.</param>
/// <param name="methodNameSuffixes">The method name suffixes to search for.</param>
/// <returns>The method filter factory.</returns>
public static MethodFilterFactory WithSuffixes(MethodFilter methodFilter, params string[] methodNameSuffixes)
=> WithPrefixAndSuffixes(methodFilter, new[] { "Add" }, methodNameSuffixes);

/// <summary>
/// Creates a method filter factory with prefixes and suffixes.
/// </summary>
/// <param name="methodFilter">The default method filter factory.</param>
/// <param name="methodNamePrefixes">The method name suffixes to search for.</param>
/// <param name="methodNameSuffixes">The method name suffixes to search for.</param>
/// <returns>The method filter factory.</returns>
public static MethodFilterFactory WithPrefixAndSuffixes(MethodFilter methodFilter, string[] methodNamePrefixes, string[] methodNameSuffixes)
{
return name =>
{
var candidates = GetCandidateNames(name, methodNamePrefixes, methodNameSuffixes);
return (methodFilter, candidates);
};

static List<string> GetCandidateNames(string name, string[] methodNamePrefixes, string[] candidateSuffixes)
{
const char GenericTypeMarker = '`';
var namesplit = name.Split(GenericTypeMarker);

var result = new List<string> { 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;
}
}
}
}
24 changes: 24 additions & 0 deletions src/ConfigurationProcessor.Core/MethodFilterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) almostchristian. All rights reserved.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Reflection;

namespace ConfigurationProcessor.Core
{
/// <summary>
/// Delegate for filtering a candidate method.
/// </summary>
/// <param name="methodInfo">The method to evaluate.</param>
/// <param name="name">The configuration name from the method filter factory.</param>
/// <returns>True if the method is acceptable.</returns>
public delegate bool MethodFilter(MethodInfo methodInfo, string name);

/// <summary>
/// Creates method filters. See <see cref="MethodFilterFactories"/> for generating factories.
/// </summary>
/// <param name="name">The configuration name.</param>
/// <returns>Returns the method filter and the candidate names.</returns>
public delegate (MethodFilter Filter, IEnumerable<string> CandidateNames) MethodFilterFactory(string name);
}
Loading

0 comments on commit fdfe28a

Please sign in to comment.