Skip to content

Commit e91fca4

Browse files
authored
Merge pull request #3 from gowon/develop
Develop
2 parents f6473ab + 2e87eb5 commit e91fca4

13 files changed

+623
-126
lines changed

.github/workflows/continuous-build.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ jobs:
4545
reporter: dotnet-trx # Format of test results
4646

4747
- name: Upload coverage to Codecov
48-
uses: codecov/codecov-action@v1
48+
uses: codecov/codecov-action@v2
4949
with:
5050
token: ${{ secrets.CODECOV_TOKEN }}
51-
file: ./test-results/*.xml
51+
directory: ./test-results/
52+
verbose: true
5253

5354
- name: Nuget Push
5455
run: nuget push ${{ env.GITHUB_WORKSPACE }}/**/*.nupkg -Source https://f.feedz.io/gowon/pre-release/nuget/index.json -ApiKey ${{ secrets.FEEDZ_API_KEY }} -SkipDuplicate

README.md

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,19 @@ dotnet add package Extensions.Options.AutoBinder
2525

2626
## Usage
2727

28-
Create a strongly typed object that you want to bind to the configuration provider:
28+
Registering your options gives you access to the following from the dependency injection container:
29+
30+
- `TOptions` - Same as `IOptions<TOptions>`, it represents configuration data once when the application starts and any changes in configuration will require the application to be restarted. It is unwrapped from the `IOptions<>` interface so that consuming interfaces do not have to force a dependency on the pattern. It is registered in the dependency injection container with a singleton lifetime.
31+
32+
- `IOptions<TOptions>` - Represents configuration data once when the application starts and any changes in configuration will require the application to be restarted. It is registered in the dependency injection container with a singleton lifetime.
33+
34+
- `IOptionsSnapshot<TOptions>` - Represents configuration on every request. Any changes in configuration while the application is running will be available for new requests without the need to restart the application. It is registered in the dependency injection container as a scoped lifetime.
35+
36+
- `IOptionsMonitor<TOptions>` - Is a service used to retrieve options and manage options notifications for TOptions instances. It is registered in the dependency injection container as a singleton lifetime.
37+
38+
### Conventional Binding
39+
40+
Create a strongly typed objects that you want to bind to the configuration provider:
2941

3042
```csharp
3143
public class SampleOptions
@@ -43,18 +55,16 @@ Strongly typed options are registered as described in the [Options pattern](http
4355
public void ConfigureServices(IServiceCollection services)
4456
{
4557
\\...
46-
4758
services.AddOptions<SampleOptions>().AutoBind();
59+
\\...
4860
}
4961
```
5062

51-
The optional parameter `suffix` can be used to indicated a suffix phrase (default: `Options`) that can be removed from the class name while binding. This allows the tooling to match the option class to a configuration section without the suffix phrase.
52-
53-
For example, the following JSON segment would be successfully bound to the `SampleOptions` class:
63+
The library will attempt to match the strongly typed object to a configuration section following a simple convention: Using the type name of the object with and without the suffix `Options`. In the case of our example class, it will be bound to any section in the configuration with the name `Sample` or `SampleOptions`. The following JSON segment would be successfully bound to the `SampleOptions` class:
5464

5565
```json
5666
{
57-
"Sample": { \\ or "SampleOptions"
67+
"Sample": {
5868
"StringVal": "Orange",
5969
"IntVal": 999,
6070
"BoolVal": true,
@@ -63,15 +73,59 @@ For example, the following JSON segment would be successfully bound to the `Samp
6373
}
6474
```
6575

66-
This gives you access to the following from the dependency injection container:
76+
### Declarative Binding
6777

68-
- `IOptions<TOptions>` - Represents configuration data once when the application starts and any changes in configuration will require the application to be restarted. It is registered in the dependency injection container with a singleton lifetime.
78+
Create strongly typed objects and apply the `AutoBind` attribute to the ones that you want to bind to the configuration provider. There is an optional parameter to specify the keys that you would like to bind to in the configuration:
6979

70-
- `TOptions` - Same as `IOptions<TOptions>`, it represents configuration data once when the application starts and any changes in configuration will require the application to be restarted. It is unwrapped from the `IOptions<>` interface so that consuming interfaces do not have to force a dependency on the pattern. It is registered in the dependency injection container with a singleton lifetime.
80+
```csharp
81+
[AutoBind("Squirrels", "Settings")]
82+
public class OtherSampleOptions
83+
{
84+
public string StringVal { get; set; }
85+
public int? IntVal { get; set; }
86+
public bool? BoolVal { get; set; }
87+
public DateTime? DateVal { get; set; }
88+
}
7189

72-
- `IOptionsSnapshot<TOptions>` - Represents configuration on every request. Any changes in configuration while the application is running will be available for new requests without the need to restart the application. It is registered in the dependency injection container as a scoped lifetime.
90+
[AutoBind]
91+
public class OtherStuff
92+
{
93+
public string StringVal { get; set; }
94+
public int? IntVal { get; set; }
95+
public bool? BoolVal { get; set; }
96+
public DateTime? DateVal { get; set; }
97+
}
98+
```
7399

74-
- `IOptionsMonitor<TOptions>` - Is a service used to retrieve options and manage options notifications for TOptions instances. It is registered in the dependency injection container as a singleton lifetime.
100+
You can scan one or more assemblies for types that have been decorated with the `AutoBind` attribute by using the registration helper `AutoBindOptions()`:
101+
102+
```csharp
103+
public void ConfigureServices(IServiceCollection services)
104+
{
105+
\\...
106+
services.AutoBindOptions();
107+
\\...
108+
}
109+
```
110+
111+
The library will attempt to match all strongly typed objects a configuration section using the default convention unless keys are specified; each key will be attempted in the order they are declared in the attribute. In the following JSON example, the `OtherSampleOptions` object would be bound to the `Settings` section and `OtherStuff` object would be bound to the `OtherStuffOptions` section:
112+
113+
```json
114+
{
115+
"Settings": {
116+
"StringVal": "Orange",
117+
"IntVal": 999,
118+
"BoolVal": true,
119+
"DateVal": "2020-07-11T07:43:29-4:00"
120+
},
121+
"OtherStuffOptions": {
122+
"StringVal": "Orange",
123+
"IntVal": 999,
124+
"BoolVal": true,
125+
"DateVal": "2020-07-11T07:43:29-4:00"
126+
}
127+
}
128+
```
75129

76130
## License
77131

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Extensions.Options.AutoBinder
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Specifies the keys to match and bind the class to data in
7+
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
8+
/// </summary>
9+
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
10+
public sealed class AutoBindAttribute : Attribute
11+
{
12+
/// <summary>
13+
/// The list of keys to match when binding the class to
14+
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
15+
/// </summary>
16+
public string[] Keys { get; }
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="T:AutoBindAttribute" /> class with the specified list of keys.
20+
/// </summary>
21+
/// <param name="keys">
22+
/// The list of keys to match when binding the class to
23+
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
24+
/// </param>
25+
public AutoBindAttribute(params string[] keys)
26+
{
27+
Keys = keys;
28+
}
29+
}
30+
}

src/Extensions.Options.AutoBinder/AutoBindingConfigurationExtensions.cs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public static class AutoBindingConfigurationExtensions
1515
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
1616
/// <param name="configuration">The configuration instance to bind.</param>
1717
/// <param name="options">The instance of <typeparamref name="TOptions" /> to bind.</param>
18+
/// <param name="key">The key to match when <typeparamref name="TOptions" /> to the configuration instance.</param>
1819
/// <param name="foundSection">
1920
/// When this method returns, contains the matching
2021
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" /> object, or null if a matching section does not
@@ -24,11 +25,20 @@ public static class AutoBindingConfigurationExtensions
2425
/// true if <paramref name="options">s</paramref> was bound to the configuration instance successfully; otherwise,
2526
/// false.
2627
/// </returns>
27-
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options,
28+
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options, string key,
2829
out IConfiguration foundSection)
2930
where TOptions : class
3031
{
31-
return TryBind(configuration, options, Constants.DefaultOptionsSuffix, out foundSection);
32+
foundSection = null;
33+
var section = configuration.GetSection(key);
34+
if (section.Exists())
35+
{
36+
foundSection = section;
37+
configuration.Bind(key, options);
38+
return true;
39+
}
40+
41+
return false;
3242
}
3343

3444
/// <summary>
@@ -38,10 +48,7 @@ public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions
3848
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
3949
/// <param name="configuration">The configuration instance to bind.</param>
4050
/// <param name="options">The instance of <typeparamref name="TOptions" /> to bind.</param>
41-
/// <param name="suffix">
42-
/// The suffix to be removed from the type name when binding <typeparamref name="TOptions" /> to the
43-
/// configuration instance.
44-
/// </param>
51+
/// <param name="keys">The list of keys to match when <typeparamref name="TOptions" /> to the configuration instance.</param>
4552
/// <param name="foundSection">
4653
/// When this method returns, contains the matching
4754
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" /> object, or null if a matching section does not
@@ -52,28 +59,20 @@ public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions
5259
/// false.
5360
/// </returns>
5461
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options,
55-
string suffix, out IConfiguration foundSection)
62+
IEnumerable<string> keys, out IConfiguration foundSection)
5663
where TOptions : class
5764
{
5865
foundSection = null;
59-
var name = typeof(TOptions).Name;
60-
var keys = new List<string>
61-
{
62-
name
63-
};
64-
65-
if (name.EndsWith(suffix))
66+
if (keys == null)
6667
{
67-
keys.Add(name.Remove(name.Length - suffix.Length));
68+
return false;
6869
}
6970

7071
foreach (var key in keys)
7172
{
72-
var section = configuration.GetSection(key);
73-
if (section.Exists())
73+
var found = configuration.TryBind(options, key, out foundSection);
74+
if (found)
7475
{
75-
foundSection = section;
76-
configuration.Bind(key, options);
7776
return true;
7877
}
7978
}

src/Extensions.Options.AutoBinder/AutoBindingOptionsBuilderExtensions.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
namespace Extensions.Options.AutoBinder
22
{
33
using System;
4+
using System.Collections.Generic;
5+
using System.Reflection;
46
using Microsoft.Extensions.Configuration;
57
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
69
using Microsoft.Extensions.Options;
710

811
/// <summary>
@@ -12,35 +15,60 @@ public static class AutoBindingOptionsBuilderExtensions
1215
{
1316
/// <summary>
1417
/// Automatically binds an instance of <typeparamref name="TOptions" /> to data in configuration providers and adds it
15-
/// to the DI container if the
16-
/// type hasn't already been registered.
18+
/// to the DI container if the type hasn't already been registered.
1719
/// </summary>
1820
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
1921
/// <param name="builder">The <see cref="T:Microsoft.Extensions.Options.OptionsBuilder`1" /> instance.</param>
20-
/// <param name="suffix">
21-
/// The suffix to be removed from the type name when binding <typeparamref name="TOptions" /> to the
22-
/// configuration instance.
22+
/// <param name="keys">
23+
/// The list of keys to match when binding <typeparamref name="TOptions" /> to the configuration
24+
/// instance.
2325
/// </param>
2426
/// <returns>The <see cref="T:Microsoft.Extensions.Options.OptionsBuilder`1" />.</returns>
2527
public static OptionsBuilder<TOptions> AutoBind<TOptions>(this OptionsBuilder<TOptions> builder,
26-
string suffix = Constants.DefaultOptionsSuffix)
28+
params string[] keys)
2729
where TOptions : class
2830
{
2931
builder = builder ?? throw new ArgumentNullException(nameof(builder));
3032

31-
builder.Configure<IConfiguration>((option, configuration) => configuration.TryBind(option, suffix, out _));
33+
var match = new List<string>();
3234

33-
builder.Services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(provider =>
35+
var attribute = typeof(TOptions).GetCustomAttribute<AutoBindAttribute>();
36+
if (keys.Length > 0)
37+
{
38+
match.AddRange(keys);
39+
}
40+
else if (attribute != null && attribute.Keys.Length > 0)
41+
{
42+
match.AddRange(attribute.Keys);
43+
}
44+
else
45+
{
46+
var name = typeof(TOptions).Name;
47+
match.Add(name);
48+
49+
if (name.EndsWith(Constants.DefaultOptionsSuffix))
50+
{
51+
match.Add(name.Remove(name.Length - Constants.DefaultOptionsSuffix.Length));
52+
}
53+
else
54+
{
55+
match.Add($"{name}{Constants.DefaultOptionsSuffix}");
56+
}
57+
}
58+
59+
builder.Configure<IConfiguration>((option, configuration) => configuration.TryBind(option, match, out _));
60+
61+
builder.Services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsChangeTokenSource<TOptions>), provider =>
3462
{
3563
var configuration = provider.GetRequiredService<IConfiguration>();
3664
return new ConfigurationChangeTokenSource<TOptions>(configuration);
37-
});
65+
}));
3866

39-
builder.Services.AddSingleton(provider =>
67+
builder.Services.TryAdd(ServiceDescriptor.Singleton(typeof(TOptions), provider =>
4068
{
4169
var options = provider.GetRequiredService<IOptions<TOptions>>();
4270
return options.Value;
43-
});
71+
}));
4472

4573
return builder;
4674
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
namespace Extensions.Options.AutoBinder
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Reflection;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
/// <summary>
10+
/// Extension methods for automatically binding strongly typed options to data in configuration providers.
11+
/// </summary>
12+
public static class AutoBindingServiceCollectionExtensions
13+
{
14+
/// <summary>
15+
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
16+
/// </summary>
17+
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
18+
/// <returns>The <see cref="IServiceCollection" />.</returns>
19+
public static IServiceCollection AutoBindOptions(this IServiceCollection services)
20+
{
21+
return AutoBindOptions(services, Assembly.GetCallingAssembly());
22+
}
23+
24+
/// <summary>
25+
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
26+
/// </summary>
27+
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
28+
/// <param name="markerType">Marker type of the assembly to scan.</param>
29+
/// <param name="additionalTypes">Additional marker types of the assemblies to scan.</param>
30+
/// <returns>The <see cref="IServiceCollection" />.</returns>
31+
public static IServiceCollection AutoBindOptions(this IServiceCollection services, Type markerType, params
32+
Type[] additionalTypes)
33+
{
34+
return AutoBindOptions(services, markerType.Assembly,
35+
additionalTypes.Select(type => type.Assembly).ToArray());
36+
}
37+
38+
/// <summary>
39+
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
40+
/// </summary>
41+
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
42+
/// <param name="assembly">Assembly to scan.</param>
43+
/// <param name="additionalAssemblies">Additional assemblies to scan.</param>
44+
/// <returns>The <see cref="IServiceCollection" />.</returns>
45+
public static IServiceCollection AutoBindOptions(this IServiceCollection services, Assembly assembly, params
46+
Assembly[] additionalAssemblies)
47+
{
48+
_ = services ?? throw new ArgumentNullException(nameof(services));
49+
services.AddOptions();
50+
51+
var assemblies = additionalAssemblies.Prepend(assembly).Distinct();
52+
var types = assemblies.SelectMany(GetTypesWithAttribute<AutoBindAttribute>);
53+
54+
var optionsMethod = typeof(OptionsServiceCollectionExtensions).GetMethods().Single(
55+
methodInfo =>
56+
methodInfo.Name == nameof(OptionsServiceCollectionExtensions.AddOptions) &&
57+
methodInfo.GetGenericArguments().Length == 1 &&
58+
methodInfo.GetParameters().Length == 1 &&
59+
methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection));
60+
61+
var binderMethod = typeof(AutoBindingOptionsBuilderExtensions).GetMethod(
62+
nameof(AutoBindingOptionsBuilderExtensions.AutoBind),
63+
BindingFlags.Static | BindingFlags.Public);
64+
65+
foreach (var optionsType in types)
66+
{
67+
var genericOptionsMethod = optionsMethod.MakeGenericMethod(optionsType);
68+
var builder = genericOptionsMethod.Invoke(null, new object[] { services });
69+
var genericBinderMethod = binderMethod!.MakeGenericMethod(optionsType);
70+
genericBinderMethod.Invoke(null, new[] { builder, new string[] { } });
71+
}
72+
73+
return services;
74+
}
75+
76+
private static IEnumerable<Type> GetTypesWithAttribute<TAttribute>(Assembly assembly)
77+
{
78+
return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(TAttribute), true).Length > 0);
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)