Skip to content

Commit

Permalink
- Fixed broken overload resolution and added test.
Browse files Browse the repository at this point in the history
- Improved readme
  • Loading branch information
Donn Relacion committed Jul 2, 2022
1 parent 6218a34 commit de86b5c
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 14 deletions.
115 changes: 109 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -21,7 +21,7 @@ services.Configure<CookiePolicyOptions>(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");
Expand All @@ -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
## 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<MyServiceOptions> 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<T>(this IServiceCollection services, T value);
```
```json
{
"MyService<System.String>" : {
"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<string, int> values);
```
```json
{
"MyService" : {
"Value1": 1,
"Value2": 2
}
}
```
34 changes: 30 additions & 4 deletions src/ConfigurationProcessor.Core/Implementation/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, int>(
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))
{
Expand Down
13 changes: 12 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
</PropertyGroup>

<PropertyGroup>
<Version>0.9.0</Version>
<Version>0.9.1</Version>
<Authors>almostchristian</Authors>
<PackageProjectUrl>https://github.com/almostchristian/ConfigurationProcessor.DependencyInjection</PackageProjectUrl>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DebugType>Portable</DebugType>
Expand All @@ -16,8 +18,17 @@
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Nullable>enable</Nullable>
<PackageTags>dependency injection;configuration;</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,63 @@ public void AddingSimpleIntWithoutParameterNameSyntax()
Assert.Collection(
serviceCollection,
sd => Assert.Equal(typeof(SimpleValue<int>), 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<string>), 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<int>), 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<object>), sd.ServiceType));
}

[Fact]
public void AddingSimpleDelegateWithoutParameterNameSyntax()
{
var json = @$"
Expand Down
19 changes: 18 additions & 1 deletion tests/TestDummies/DummyServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace TestDummies
[ExcludeFromCodeCoverage]
public static class DummyServiceCollectionExtensions
{

public static IServiceCollection AddDummyDelegate(this IServiceCollection services, DummyDelegate dummyDelegate)
{
services.AddSingleton(dummyDelegate);
Expand Down Expand Up @@ -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<string>(stringValue));
return services;
}

public static IServiceCollection AddSimpleValue(this IServiceCollection services, int intValue)
{
services.AddSingleton(new SimpleValue<int>(intValue));
return services;
}

public static IServiceCollection AddSimpleValue(this IServiceCollection services)
{
services.AddSingleton(new SimpleValue<object>(new object()));
return services;
}

public static IServiceCollection AddSimpleDelegate(this IServiceCollection services, Delegate value)
{
services.AddSingleton(new SimpleValue<Delegate>(value));
Expand Down

0 comments on commit de86b5c

Please sign in to comment.