diff --git a/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs b/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs index 2b5cab3..6677679 100644 --- a/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs +++ b/src/ConfigurationProcessor.Core/ConfigurationExtensions.cs @@ -43,41 +43,71 @@ public static TContext ProcessConfiguration( throw new ArgumentNullException(nameof(configuration)); } + configuration.ProcessConfiguration( + context, + options => + { + options.ConfigSection = configSection; + options.MethodFilterFactory = methodFilterFactory; + options.AdditionalMethods = additionalMethods ?? Enumerable.Empty(); + options.ContextPaths = contextPaths; + }); + return context; + } + + /// + /// Processes the configuration. + /// + /// The object type that is transformed by the configuration. + /// The configuration object. + /// The object that is processed by the configuration. + /// The configuration reader options builder. + /// The object for chaining. + /// Thrown when is null. + public static TContext ProcessConfiguration( + this IConfiguration configuration, + TContext context, + Action 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(), - AssemblyFinder.Auto()); + AssemblyFinder.Auto(), + configureOptions); return context; } internal static TConfig AddFromConfiguration( this TConfig builder, IConfiguration rootConfiguration, - IConfigurationSection configurationSection, - string?[] servicePaths, - MethodFilterFactory? methodFilterFactory, - MethodInfo[] additionalMethods, - AssemblyFinder assemblyFinder) + AssemblyFinder assemblyFinder, + Action configureOptions) where TConfig : class { - var reader = new ConfigurationReader(rootConfiguration, configurationSection, assemblyFinder, additionalMethods); + var options = new ConfigurationReaderOptions(); + configureOptions?.Invoke(options); + var configurationSection = rootConfiguration.GetSection(options.ConfigSection); + + var reader = new ConfigurationReader(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); } } diff --git a/src/ConfigurationProcessor.Core/ConfigurationReaderOptions.cs b/src/ConfigurationProcessor.Core/ConfigurationReaderOptions.cs new file mode 100644 index 0000000..fbb9da5 --- /dev/null +++ b/src/ConfigurationProcessor.Core/ConfigurationReaderOptions.cs @@ -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 +{ + /// + /// Configuration reader options. + /// + public class ConfigurationReaderOptions + { + /// + /// Gets the config section. + /// + public string ConfigSection { get; set; } = "Services"; + + /// + /// Gets the context paths. + /// + public IEnumerable? ContextPaths { get; set; } + + /// + /// Gets ths method filter factory. + /// + public MethodFilterFactory? MethodFilterFactory { get; set; } + + /// + /// Additional methods. + /// + public IEnumerable AdditionalMethods { get; set; } = Enumerable.Empty(); + + /// + /// Gets or sets the method to invoke when a method is not found. The default method throws a . + /// + public Action OnExtensionMethodNotFound { get; set; } = x => { }; + } +} diff --git a/src/ConfigurationProcessor.Core/ExtensionMethodNotFoundEventArgs.cs b/src/ConfigurationProcessor.Core/ExtensionMethodNotFoundEventArgs.cs new file mode 100644 index 0000000..13ff617 --- /dev/null +++ b/src/ConfigurationProcessor.Core/ExtensionMethodNotFoundEventArgs.cs @@ -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 +{ + /// + /// Represents event data when an extension method was not found. + /// + public class ExtensionMethodNotFoundEventArgs : EventArgs + { + internal ExtensionMethodNotFoundEventArgs( + IEnumerable candidateMethods, + IEnumerable candidateNames, + Type extensionMethodType, + IReadOnlyDictionary suppliedArguments) + { + CandidateMethods = candidateMethods; + CandidateNames = candidateNames; + ExtensionMethodType = extensionMethodType; + SuppliedArguments = suppliedArguments; + } + + /// + /// Gets the candidate methods. + /// + public IEnumerable CandidateMethods { get; } + + /// + /// Gets the candidate method names. + /// + public IEnumerable CandidateNames { get; } + + /// + /// Gets the extension method type that was searched. + /// + public Type ExtensionMethodType { get; } + + /// + /// Gets the supplied argument names. + /// + public IReadOnlyDictionary SuppliedArguments { get; } + + /// + /// Gets or sets if the event is handled. If true, an exception will not be thrown. + /// + public bool Handled { get; set; } + } +} diff --git a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs index 6cca80e..acec799 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ConfigurationReader{TConfig}.cs @@ -2,6 +2,7 @@ // Copyright (c) almostchristian. All rights reserved. // ------------------------------------------------------------------------------------------------- +using System; using System.Linq; using System.Reflection; using ConfigurationProcessor.Core.Assemblies; @@ -12,12 +13,12 @@ namespace ConfigurationProcessor.Core.Implementation internal class ConfigurationReader : ConfigurationReader, IConfigurationReader 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()) @@ -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) diff --git a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs index a264af9..31f1741 100644 --- a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs @@ -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 FindConfigurationExtensionMethods( this ResolutionContext resolutionContext, string key, @@ -335,7 +349,7 @@ private static Delegate GenerateLambda( { var methodExpressions = new List(); - 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 { originalKey }; if (int.TryParse(sourceConfigurationSection.Key, out _)) diff --git a/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs b/src/ConfigurationProcessor.Core/Implementation/IConfigurationReader.cs index 50c4934..a8b2590 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, MethodFilterFactory methodFilterFactory); + void AddServices(TConfig builder, string? sectionName, bool getChildren, ConfigurationReaderOptions options); } } diff --git a/src/ConfigurationProcessor.Core/Implementation/ResolutionContext.cs b/src/ConfigurationProcessor.Core/Implementation/ResolutionContext.cs index d728f0e..72c3ed4 100644 --- a/src/ConfigurationProcessor.Core/Implementation/ResolutionContext.cs +++ b/src/ConfigurationProcessor.Core/Implementation/ResolutionContext.cs @@ -26,7 +26,8 @@ public ResolutionContext( AssemblyFinder assemblyFinder, IConfiguration rootConfiguration, IConfigurationSection appConfiguration, - MethodInfo[] additionalMethods, + IEnumerable additionalMethods, + Action onExtensionMethodNotFound, params Type[] markerTypes) { if (assemblyFinder != null && appConfiguration != null) @@ -38,13 +39,16 @@ public ResolutionContext( ConfigurationAssemblies = new List(); } + OnExtensionMethodNotFound = onExtensionMethodNotFound; this.appConfiguration = appConfiguration; this.rootConfiguration = rootConfiguration; AssemblyFinder = assemblyFinder!; AdditionalMethods = additionalMethods; } - public MethodInfo[] AdditionalMethods { get; } + public Action OnExtensionMethodNotFound { get; } + + public IEnumerable AdditionalMethods { get; } public AssemblyFinder AssemblyFinder { get; } diff --git a/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs b/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs index b1dc1f8..18c8aca 100644 --- a/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs +++ b/src/ConfigurationProcessor.DependencyInjection/ConfigurationProcessorServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Linq; using System.Reflection; using ConfigurationProcessor.Core; using Microsoft.Extensions.Configuration; @@ -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(); + }); + + /// + /// Adds services from configuration. + /// + /// The service collection. + /// The configuration to read from. + /// The config options. + /// The service collection for chaining. + /// Thrown when is null. + public static IServiceCollection AddFromConfiguration( + this IServiceCollection services, + IConfiguration configuration, + Action configureOptions) { if (configuration == null) { @@ -79,10 +100,7 @@ public static IServiceCollection AddFromConfiguration( return configuration.ProcessConfiguration( services, - servicesSection, - servicePaths, - methodFilterFactory, - additionalMethods); + configureOptions); } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d1977dc..5dc2544 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ - 1.1.1 + 1.2.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.2.0 + - Added interception for missing extension method event v1.1.1 - Ignore TypeLoadException when scanning for extension methods v1.1.0 diff --git a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationReaderTests.cs b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationReaderTests.cs index 907149a..885716b 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationReaderTests.cs +++ b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationReaderTests.cs @@ -40,7 +40,7 @@ public ConfigurationReaderTests() rootConfig, rootConfig.GetSection("FhirEngine"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), - Array.Empty()); + new Core.ConfigurationReaderOptions()); } [Fact] @@ -107,7 +107,7 @@ public void AddServicesSupportExpandedSyntaxWithArgs() kvp => { Assert.Equal("mappings", kvp.Key); - Assert.Equal("{Message}", kvp.Value.ArgName.ConvertTo(default, typeof(string), new ResolutionContext(null, (IConfiguration)null, null, null))); + Assert.Equal("{Message}", kvp.Value.ArgName.ConvertTo(default, typeof(string), new ResolutionContext(null, (IConfiguration)null, null, null, _ => { }))); }); } @@ -140,7 +140,7 @@ public void AddServicesSupportAlternateSyntaxWithArgs() kvp => { Assert.Equal("mappings", kvp.Key); - Assert.Equal("{Message}", kvp.Value.ArgName.ConvertTo(default, typeof(string), new ResolutionContext(null, (IConfiguration)null, null, null))); + Assert.Equal("{Message}", kvp.Value.ArgName.ConvertTo(default, typeof(string), new ResolutionContext(null, (IConfiguration)null, null, null, _ => { }))); }); } }