diff --git a/README.md b/README.md index 81ede1b..d327ff3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # ConfigurationProcessor.DependencyInjection -This library registers and configures services in an `IServiceCollection` using .NET's' configuration library. +This library registers and configures services in a service collection using .NET's' configuration library. ## Example -Given an application with a configure services section like below: +Given an application with a `ConfigureServices` section like below: ```csharp services.AddLogging(); @@ -21,7 +21,7 @@ services.Configure(options => }); ``` -The configure services method above can be moved to the configuration as in below: +The `ConfigureServices` method above can be moved to the configuration using the code and the appsettings.config configuration as in below: ```csharp services.AddFromConfiguration(Configuration, "Services"); @@ -44,7 +44,110 @@ services.AddFromConfiguration(Configuration, "Services"); } ``` -## Features -List down the features and limitations +Since we are using `IConfiguration`, we aren't limited to the appsettings.json for configuring our services. We can also have the configuration in environment variables, in command-line arguments or with custom configuration providers such as AWS Secrets Manager. -Under construction \ No newline at end of file +## Basics + +The library works by using reflection and scanning all currently loaded assemblies for extension methods for `IServiceCollection`. + +### Extension method mapping and overload resolution +Given a configuration named `Logging`, an extension method named `AddLogging` or `Logging` will be filtered from the candidate extension methods. If multiple candidates are found, the best overload will be chosen based on the name of the input parameters. + +Given the following extension methods: +```csharp +public IServiceCollection AddMyPlugin(this IServiceCollection services); +public IServiceCollection AddMyPlugin(this IServiceCollection services, string name); +public IServiceCollection AddMyPlugin(this IServiceCollection services, int count); +``` + +When given the configuration below, the extension method `AddMyPlugin(IServiceCollection, int)` is chosen. +```json +{ + "Services": { + "MyPlugin" : { + "Count": 23 + } + } +} +``` + +If the extension method is parameterless, use `true` instead of an object. The configuration method below will choose `AddMyPlugin(IServiceCollection)` +```json +{ + "Services": { + "MyPlugin" : true + } +} + +``` + +### Action Delegate mapping +ConfigurationProcessor can be used with extension methods that use an action delegate. + +Given the extension method below: +```csharp +public IServiceCollection AddMyService(this IServiceCollection services, Action configureOptions); + +public class MyServiceOptions +{ + public string Title { get; set; } +} +``` + +The configuration below is equivalent to calling `services.AddMyService(options => {});`: +```json +{ + "MyPlugin" : true +} +``` + +The configuration below is equivalent to calling `services.AddMyService(options => { options.Title = "abc" });`: +```json +{ + "MyPlugin" : { + "Title": "abc" + } +} +``` + +### Generic extension method mapping +Generic extension methods can be mapped by supplying the generic parameter via the angle brackets `<>`. The full name of the type must be supplied. +```csharp +public IServiceCollection AddMyService(this IServiceCollection services, T value); +``` +```json +{ + "MyService" : { + "Value": "hello world" + } +} +``` + +### Mapping to extension methods with a single array parameter +Extension methods that have a single array parameter can be mapped with json arrays. +```csharp +public IServiceCollection AddMyService(this IServiceCollection services, params string[] values); +``` +```json +{ + "MyService" : [ + "salut", + "hi", + "konichiwa" + ] +} +``` + +### Mapping to extension methods with a single dictionary parameter +Extension methods that have a single dictionary parameter with ***NO OVERLOADS*** can be mapped with json objects. +```csharp +public IServiceCollection AddMyService(this IServiceCollection services, Dictionary values); +``` +```json +{ + "MyService" : { + "Value1": 1, + "Value2": 2 + } +} +``` \ No newline at end of file diff --git a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs index c51fb57..7e3fc60 100644 --- a/src/ConfigurationProcessor.Core/Implementation/Extensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/Extensions.cs @@ -261,24 +261,50 @@ public static void BindMappableValues( // Per issue #111, it is safe to use case-insensitive matching on argument names. The CLR doesn't permit this type // of overloading, and the Microsoft.Extensions.Configuration keys are case-insensitive (case is preserved with some // config sources, but key-matching is case-insensitive and case-preservation does not appear to be guaranteed). - var selectedMethod = candidateMethods + var selectedMethods = candidateMethods .Where(m => m.GetParameters() .Skip(1) .All(p => p.HasImplicitValueWhenNotSpecified() || p.IsConfigurationOptionsBuilder(out _) || p.ParameterType!.ParameterTypeHasPropertyMatches(suppliedArgumentNames) || ParameterNameMatches(p.Name!, suppliedArgumentNames))) - .OrderByDescending(m => + .GroupBy(m => { var matchingArgs = m.GetParameters().Where(p => p.IsConfigurationOptionsBuilder(out _) || ParameterNameMatches(p.Name!, suppliedArgumentNames)).ToList(); // Prefer the configuration method with most number of matching arguments and of those the ones with // the most string type parameters to predict best match with least type casting - return new Tuple( + return ( matchingArgs.Count, matchingArgs.Count(p => p.ParameterType == typeof(string))); }) - .FirstOrDefault(); + .OrderByDescending(x => x.Key) + .FirstOrDefault()? + .AsEnumerable(); + + MethodInfo? selectedMethod; + if (selectedMethods?.Count() > 1) + { + // if no best match was found, use the one with a similar number of arguments based on the argument list + selectedMethods = selectedMethods.Where(m => + { + var requiredParamCount = m.GetParameters().Count(x => !x.IsOptional); + return requiredParamCount == suppliedArgumentNames.Count() + (m.IsStatic ? 1 : 0); + }); + + if (selectedMethods.Count() > 1) + { + selectedMethod = selectedMethods.OrderBy(m => m.IsStatic ? 1 : 0).FirstOrDefault(); + } + else + { + selectedMethod = selectedMethods.SingleOrDefault(); + } + } + else + { + selectedMethod = selectedMethods?.SingleOrDefault(); + } if (selectedMethod == null && suppliedArgumentNames.Count() == 1 && suppliedArgumentNames.All(string.IsNullOrEmpty)) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2b23e8d..ba76178 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,7 +6,9 @@ - 0.9.0 + 0.9.1 + almostchristian + https://github.com/almostchristian/ConfigurationProcessor.DependencyInjection true Portable @@ -16,8 +18,17 @@ true LICENSE enable + dependency injection;configuration; + README.md + + + True + \ + + + true true diff --git a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs index abb866a..2518182 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs +++ b/tests/ConfigurationProcessor.DependencyInjection.UnitTests/ConfigurationBuilderTestsBase.cs @@ -364,9 +364,63 @@ public void AddingSimpleIntWithoutParameterNameSyntax() Assert.Collection( serviceCollection, sd => Assert.Equal(typeof(SimpleValue), sd.ServiceType)); - } + } - [Fact] + [Fact] + public void AddingSimpleStringOverloadResolutionTest() + { + var json = @$" +{{ + 'Services': {{ + 'SimpleValue': {{ 'StringValue': 'hello' }} + }} +}}"; + + var serviceCollection = new TestServiceCollection(); + TestBuilder(json, serviceCollection); + + Assert.Collection( + serviceCollection, + sd => Assert.Equal(typeof(SimpleValue), sd.ServiceType)); + } + + [Fact] + public void AddingSimpleIntOverloadResolutionTest() + { + var json = @$" +{{ + 'Services': {{ + 'SimpleValue': {{ 'IntValue': 42 }} + }} +}}"; + + var serviceCollection = new TestServiceCollection(); + TestBuilder(json, serviceCollection); + + Assert.Collection( + serviceCollection, + sd => Assert.Equal(typeof(SimpleValue), sd.ServiceType)); + } + + [Fact] + public void AddingParameterlessOverloadResolutionTest() + { + var json = @$" +{{ + 'Services': {{ + 'SimpleValue': true + }} +}}"; + + var serviceCollection = new TestServiceCollection(); + TestBuilder(json, serviceCollection); + + Assert.Collection( + serviceCollection, + sd => Assert.Equal(typeof(SimpleValue), sd.ServiceType)); + } + + [Fact] public void AddingSimpleDelegateWithoutParameterNameSyntax() { var json = @$" diff --git a/tests/TestDummies/DummyServiceCollectionExtensions.cs b/tests/TestDummies/DummyServiceCollectionExtensions.cs index c3635ff..a7d4163 100644 --- a/tests/TestDummies/DummyServiceCollectionExtensions.cs +++ b/tests/TestDummies/DummyServiceCollectionExtensions.cs @@ -16,7 +16,6 @@ namespace TestDummies [ExcludeFromCodeCoverage] public static class DummyServiceCollectionExtensions { - public static IServiceCollection AddDummyDelegate(this IServiceCollection services, DummyDelegate dummyDelegate) { services.AddSingleton(dummyDelegate); @@ -69,6 +68,24 @@ public static IServiceCollection AddSimpleInt32(this IServiceCollection services return services; } + public static IServiceCollection AddSimpleValue(this IServiceCollection services, string stringValue) + { + services.AddSingleton(new SimpleValue(stringValue)); + return services; + } + + public static IServiceCollection AddSimpleValue(this IServiceCollection services, int intValue) + { + services.AddSingleton(new SimpleValue(intValue)); + return services; + } + + public static IServiceCollection AddSimpleValue(this IServiceCollection services) + { + services.AddSingleton(new SimpleValue(new object())); + return services; + } + public static IServiceCollection AddSimpleDelegate(this IServiceCollection services, Delegate value) { services.AddSingleton(new SimpleValue(value));