Skip to content

Commit

Permalink
Added interception for missing extension method event
Browse files Browse the repository at this point in the history
  • Loading branch information
almostchristian committed Sep 19, 2022
1 parent 2f70acd commit cc2bbe3
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 47 deletions.
60 changes: 45 additions & 15 deletions src/ConfigurationProcessor.Core/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,71 @@ public static TContext ProcessConfiguration<TContext>(
throw new ArgumentNullException(nameof(configuration));
}

configuration.ProcessConfiguration(
context,
options =>
{
options.ConfigSection = configSection;
options.MethodFilterFactory = methodFilterFactory;
options.AdditionalMethods = additionalMethods ?? Enumerable.Empty<MethodInfo>();
options.ContextPaths = contextPaths;
});
return context;
}

/// <summary>
/// Processes the configuration.
/// </summary>
/// <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="configureOptions">The configuration reader options builder.</param>
/// <returns>The <paramref name="context"/> object for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configuration"/> is null.</exception>
public static TContext ProcessConfiguration<TContext>(
this IConfiguration configuration,
TContext context,
Action<ConfigurationReaderOptions> configureOptions)
where TContext : class
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}

context.AddFromConfiguration(
configuration,
configuration.GetSection(configSection),
contextPaths ?? new string?[] { string.Empty },
methodFilterFactory,
additionalMethods ?? Array.Empty<MethodInfo>(),
AssemblyFinder.Auto());
AssemblyFinder.Auto(),
configureOptions);
return context;
}

internal static TConfig AddFromConfiguration<TConfig>(
this TConfig builder,
IConfiguration rootConfiguration,
IConfigurationSection configurationSection,
string?[] servicePaths,
MethodFilterFactory? methodFilterFactory,
MethodInfo[] additionalMethods,
AssemblyFinder assemblyFinder)
AssemblyFinder assemblyFinder,
Action<ConfigurationReaderOptions> configureOptions)
where TConfig : class
{
var reader = new ConfigurationReader<TConfig>(rootConfiguration, configurationSection, assemblyFinder, additionalMethods);
var options = new ConfigurationReaderOptions();
configureOptions?.Invoke(options);
var configurationSection = rootConfiguration.GetSection(options.ConfigSection);

var reader = new ConfigurationReader<TConfig>(rootConfiguration, configurationSection, assemblyFinder, options);

foreach (var servicePath in servicePaths)
foreach (var servicePath in options.ContextPaths ?? new string[] { string.Empty })
{
if (string.IsNullOrEmpty(servicePath))
{
reader.AddServices(builder, null, true, methodFilterFactory);
reader.AddServices(builder, null, true, options);
}
else if (servicePath![0] == '^')
{
reader.AddServices(builder, servicePath.Substring(1), false, methodFilterFactory);
reader.AddServices(builder, servicePath.Substring(1), false, options);
}
else
{
reader.AddServices(builder, servicePath, true, methodFilterFactory);
reader.AddServices(builder, servicePath, true, options);
}
}

Expand Down
43 changes: 43 additions & 0 deletions src/ConfigurationProcessor.Core/ConfigurationReaderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) almostchristian. All rights reserved.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ConfigurationProcessor.Core.Implementation;

namespace ConfigurationProcessor.Core
{
/// <summary>
/// Configuration reader options.
/// </summary>
public class ConfigurationReaderOptions
{
/// <summary>
/// Gets the config section.
/// </summary>
public string ConfigSection { get; set; } = "Services";

/// <summary>
/// Gets the context paths.
/// </summary>
public IEnumerable<string>? ContextPaths { get; set; }

/// <summary>
/// Gets ths method filter factory.
/// </summary>
public MethodFilterFactory? MethodFilterFactory { get; set; }

/// <summary>
/// Additional methods.
/// </summary>
public IEnumerable<MethodInfo> AdditionalMethods { get; set; } = Enumerable.Empty<MethodInfo>();

/// <summary>
/// Gets or sets the method to invoke when a method is not found. The default method throws a <see cref="MissingMethodException"/>.
/// </summary>
public Action<ExtensionMethodNotFoundEventArgs> OnExtensionMethodNotFound { get; set; } = x => { };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) almostchristian. All rights reserved.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Configuration;

namespace ConfigurationProcessor.Core
{
/// <summary>
/// Represents event data when an extension method was not found.
/// </summary>
public class ExtensionMethodNotFoundEventArgs : EventArgs
{
internal ExtensionMethodNotFoundEventArgs(
IEnumerable<MethodInfo> candidateMethods,
IEnumerable<string> candidateNames,
Type extensionMethodType,
IReadOnlyDictionary<string, IConfigurationSection> suppliedArguments)
{
CandidateMethods = candidateMethods;
CandidateNames = candidateNames;
ExtensionMethodType = extensionMethodType;
SuppliedArguments = suppliedArguments;
}

/// <summary>
/// Gets the candidate methods.
/// </summary>
public IEnumerable<MethodInfo> CandidateMethods { get; }

/// <summary>
/// Gets the candidate method names.
/// </summary>
public IEnumerable<string> CandidateNames { get; }

/// <summary>
/// Gets the extension method type that was searched.
/// </summary>
public Type ExtensionMethodType { get; }

/// <summary>
/// Gets the supplied argument names.
/// </summary>
public IReadOnlyDictionary<string, IConfigurationSection> SuppliedArguments { get; }

/// <summary>
/// Gets or sets if the event is handled. If true, an exception will not be thrown.
/// </summary>
public bool Handled { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright (c) almostchristian. All rights reserved.
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Reflection;
using ConfigurationProcessor.Core.Assemblies;
Expand All @@ -12,12 +13,12 @@ namespace ConfigurationProcessor.Core.Implementation
internal class ConfigurationReader<TConfig> : ConfigurationReader, IConfigurationReader<TConfig>
where TConfig : class
{
public ConfigurationReader(IConfiguration configuration, IConfigurationSection configSection, AssemblyFinder assemblyFinder, MethodInfo[] additionalMethods)
: base(new ResolutionContext(assemblyFinder, configuration, configSection, additionalMethods, typeof(TConfig)), configuration, assemblyFinder, configSection)
public ConfigurationReader(IConfiguration configuration, IConfigurationSection configSection, AssemblyFinder assemblyFinder, ConfigurationReaderOptions options)
: base(new ResolutionContext(assemblyFinder, configuration, configSection, options.AdditionalMethods, options.OnExtensionMethodNotFound, typeof(TConfig)), configuration, assemblyFinder, configSection)
{
}

public void AddServices(TConfig builder, string? sectionName, bool getChildren, MethodFilterFactory? methodFilterFactory)
public void AddServices(TConfig builder, string? sectionName, bool getChildren, ConfigurationReaderOptions options)
{
var builderDirective = string.IsNullOrEmpty(sectionName) ? ConfigurationSection : ConfigurationSection.GetSection(sectionName);
if (!getChildren || builderDirective.GetChildren().Any())
Expand All @@ -26,7 +27,7 @@ public void AddServices(TConfig builder, string? sectionName, bool getChildren,
ResolutionContext.CallConfigurationMethods(
typeof(TConfig),
methodCalls,
methodFilterFactory,
options.MethodFilterFactory,
(arguments, methodInfo) =>
{
if (methodInfo.IsStatic)
Expand Down
48 changes: 31 additions & 17 deletions src/ConfigurationProcessor.Core/Implementation/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,30 +114,44 @@ public static void CallConfigurationMethods(
{
if (!configurationMethods.Any())
{
var allExtensionMethods = resolutionContext
configurationMethods = resolutionContext
.FindConfigurationExtensionMethods(methodName, extensionArgumentType, typeArgs, null, null)
.Select(x => x.Name).Distinct();

throw new MissingMethodException($"Unable to find methods called \"{string.Join(", ", candidateNames)}\" for type '{extensionArgumentType}'. Extension method names for type are:{Environment.NewLine}{string.Join(Environment.NewLine, allExtensionMethods)}");
.Distinct();
}
else

var errorEventArgs = new ExtensionMethodNotFoundEventArgs(
configurationMethods,
candidateNames,
extensionArgumentType,
paramArgs.ToDictionary(x => x.Key, x => x.Value.ConfigSection));

resolutionContext.OnExtensionMethodNotFound(errorEventArgs);

if (!errorEventArgs.Handled)
{
var methodsByName = configurationMethods
.Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Skip(1).Select(p => p.Name))})")
.ToList();

throw new MissingMethodException($"Unable to find methods called \"{string.Join(", ", candidateNames)}\" for type '{extensionArgumentType}' "
+ (suppliedArgumentNames.Any()
? "for supplied named arguments: " + string.Join(", ", suppliedArgumentNames)
: "with no supplied arguments")
+ ". Candidate methods are:"
+ Environment.NewLine
+ string.Join(Environment.NewLine, methodsByName));
ThrowMissingMethodException(errorEventArgs);
}
}
}
}

private static void ThrowMissingMethodException(ExtensionMethodNotFoundEventArgs args)
{
string message;
var methods = args.CandidateMethods
.Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Skip(1).Select(p => p.Name))})")
.ToList();
message = $"Unable to find methods called \"{string.Join(", ", args.CandidateNames)}\" for type '{args.ExtensionMethodType}' "
+ (args.SuppliedArguments.Any()
? "for supplied named arguments: " + string.Join(", ", args.SuppliedArguments.Keys)
: "with no supplied arguments")
+ ". Candidate methods are:"
+ Environment.NewLine
+ string.Join(Environment.NewLine, methods);

throw new MissingMethodException(message);
}

private static List<MethodInfo> FindConfigurationExtensionMethods(
this ResolutionContext resolutionContext,
string key,
Expand Down Expand Up @@ -335,7 +349,7 @@ private static Delegate GenerateLambda(
{
var methodExpressions = new List<Expression>();

var childResolutionContext = new ResolutionContext(resolutionContext.AssemblyFinder, resolutionContext.RootConfiguration, sourceConfigurationSection, resolutionContext.AdditionalMethods, argumentType);
var childResolutionContext = new ResolutionContext(resolutionContext.AssemblyFinder, resolutionContext.RootConfiguration, sourceConfigurationSection, resolutionContext.AdditionalMethods, resolutionContext.OnExtensionMethodNotFound, argumentType);

var keysToExclude = new List<string> { originalKey };
if (int.TryParse(sourceConfigurationSection.Key, out _))
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, MethodFilterFactory methodFilterFactory);
void AddServices(TConfig builder, string? sectionName, bool getChildren, ConfigurationReaderOptions options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public ResolutionContext(
AssemblyFinder assemblyFinder,
IConfiguration rootConfiguration,
IConfigurationSection appConfiguration,
MethodInfo[] additionalMethods,
IEnumerable<MethodInfo> additionalMethods,
Action<ExtensionMethodNotFoundEventArgs> onExtensionMethodNotFound,
params Type[] markerTypes)
{
if (assemblyFinder != null && appConfiguration != null)
Expand All @@ -38,13 +39,16 @@ public ResolutionContext(
ConfigurationAssemblies = new List<Assembly>();
}

OnExtensionMethodNotFound = onExtensionMethodNotFound;
this.appConfiguration = appConfiguration;
this.rootConfiguration = rootConfiguration;
AssemblyFinder = assemblyFinder!;
AdditionalMethods = additionalMethods;
}

public MethodInfo[] AdditionalMethods { get; }
public Action<ExtensionMethodNotFoundEventArgs> OnExtensionMethodNotFound { get; }

public IEnumerable<MethodInfo> AdditionalMethods { get; }

public AssemblyFinder AssemblyFinder { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Reflection;
using ConfigurationProcessor.Core;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -71,6 +72,26 @@ public static IServiceCollection AddFromConfiguration(
string[]? servicePaths,
MethodFilterFactory? methodFilterFactory,
MethodInfo[]? additionalMethods = null)
=> services.AddFromConfiguration(configuration, options =>
{
options.ConfigSection = servicesSection;
options.ContextPaths = servicePaths;
options.MethodFilterFactory = methodFilterFactory;
options.AdditionalMethods = additionalMethods ?? Enumerable.Empty<MethodInfo>();
});

/// <summary>
/// Adds services from configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configuration">The configuration to read from.</param>
/// <param name="configureOptions">The config options.</param>
/// <returns>The service collection for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configuration"/> is null.</exception>
public static IServiceCollection AddFromConfiguration(
this IServiceCollection services,
IConfiguration configuration,
Action<ConfigurationReaderOptions> configureOptions)
{
if (configuration == null)
{
Expand All @@ -79,10 +100,7 @@ public static IServiceCollection AddFromConfiguration(

return configuration.ProcessConfiguration(
services,
servicesSection,
servicePaths,
methodFilterFactory,
additionalMethods);
configureOptions);
}
}
}
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.1.1</Version>
<Version>1.2.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.2.0
- Added interception for missing extension method event
v1.1.1
- Ignore TypeLoadException when scanning for extension methods
v1.1.0
Expand Down
Loading

0 comments on commit cc2bbe3

Please sign in to comment.