Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continuous-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
reporter: dotnet-trx # Format of test results

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./test-results/*.xml
Expand Down
76 changes: 65 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@ dotnet add package Extensions.Options.AutoBinder

## Usage

Create a strongly typed object that you want to bind to the configuration provider:
Registering your options gives you access to the following from the dependency injection container:

- `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.

- `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.

- `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.

- `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.

### Conventional Binding

Create a strongly typed objects that you want to bind to the configuration provider:

```csharp
public class SampleOptions
Expand All @@ -43,18 +55,16 @@ Strongly typed options are registered as described in the [Options pattern](http
public void ConfigureServices(IServiceCollection services)
{
\\...

services.AddOptions<SampleOptions>().AutoBind();
\\...
}
```

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.

For example, the following JSON segment would be successfully bound to the `SampleOptions` class:
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:

```json
{
"Sample": { \\ or "SampleOptions"
"Sample": {
"StringVal": "Orange",
"IntVal": 999,
"BoolVal": true,
Expand All @@ -63,15 +73,59 @@ For example, the following JSON segment would be successfully bound to the `Samp
}
```

This gives you access to the following from the dependency injection container:
### Declarative Binding

- `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.
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:

- `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.
```csharp
[AutoBind("Squirrels", "Settings")]
public class OtherSampleOptions
{
public string StringVal { get; set; }
public int? IntVal { get; set; }
public bool? BoolVal { get; set; }
public DateTime? DateVal { get; set; }
}

- `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.
[AutoBind]
public class OtherStuff
{
public string StringVal { get; set; }
public int? IntVal { get; set; }
public bool? BoolVal { get; set; }
public DateTime? DateVal { get; set; }
}
```

- `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.
You can scan one or more assemblies for types that have been decorated with the `AutoBind` attribute by using the registration helper `AutoBindOptions()`:

```csharp
public void ConfigureServices(IServiceCollection services)
{
\\...
services.AutoBindOptions();
\\...
}
```

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:

```json
{
"Settings": {
"StringVal": "Orange",
"IntVal": 999,
"BoolVal": true,
"DateVal": "2020-07-11T07:43:29-4:00"
},
"OtherStuffOptions": {
"StringVal": "Orange",
"IntVal": 999,
"BoolVal": true,
"DateVal": "2020-07-11T07:43:29-4:00"
}
}
```

## License

Expand Down
30 changes: 30 additions & 0 deletions src/Extensions.Options.AutoBinder/AutoBindAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Extensions.Options.AutoBinder
{
using System;

/// <summary>
/// Specifies the keys to match and bind the class to data in
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class AutoBindAttribute : Attribute
{
/// <summary>
/// The list of keys to match when binding the class to
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
/// </summary>
public string[] Keys { get; }

/// <summary>
/// Initializes a new instance of the <see cref="T:AutoBindAttribute" /> class with the specified list of keys.
/// </summary>
/// <param name="keys">
/// The list of keys to match when binding the class to
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" />.
/// </param>
public AutoBindAttribute(params string[] keys)
{
Keys = keys;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static class AutoBindingConfigurationExtensions
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="options">The instance of <typeparamref name="TOptions" /> to bind.</param>
/// <param name="key">The key to match when <typeparamref name="TOptions" /> to the configuration instance.</param>
/// <param name="foundSection">
/// When this method returns, contains the matching
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" /> object, or null if a matching section does not
Expand All @@ -24,11 +25,20 @@ public static class AutoBindingConfigurationExtensions
/// true if <paramref name="options">s</paramref> was bound to the configuration instance successfully; otherwise,
/// false.
/// </returns>
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options,
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options, string key,
out IConfiguration foundSection)
where TOptions : class
{
return TryBind(configuration, options, Constants.DefaultOptionsSuffix, out foundSection);
foundSection = null;
var section = configuration.GetSection(key);
if (section.Exists())
{
foundSection = section;
configuration.Bind(key, options);
return true;
}

return false;
}

/// <summary>
Expand All @@ -38,10 +48,7 @@ public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="options">The instance of <typeparamref name="TOptions" /> to bind.</param>
/// <param name="suffix">
/// The suffix to be removed from the type name when binding <typeparamref name="TOptions" /> to the
/// configuration instance.
/// </param>
/// <param name="keys">The list of keys to match when <typeparamref name="TOptions" /> to the configuration instance.</param>
/// <param name="foundSection">
/// When this method returns, contains the matching
/// <see cref="T:Microsoft.Extensions.Configuration.IConfiguration" /> object, or null if a matching section does not
Expand All @@ -52,28 +59,20 @@ public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions
/// false.
/// </returns>
public static bool TryBind<TOptions>(this IConfiguration configuration, TOptions options,
string suffix, out IConfiguration foundSection)
IEnumerable<string> keys, out IConfiguration foundSection)
where TOptions : class
{
foundSection = null;
var name = typeof(TOptions).Name;
var keys = new List<string>
{
name
};

if (name.EndsWith(suffix))
if (keys == null)
{
keys.Add(name.Remove(name.Length - suffix.Length));
return false;
}

foreach (var key in keys)
{
var section = configuration.GetSection(key);
if (section.Exists())
var found = configuration.TryBind(options, key, out foundSection);
if (found)
{
foundSection = section;
configuration.Bind(key, options);
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
namespace Extensions.Options.AutoBinder
{
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

/// <summary>
Expand All @@ -12,35 +15,60 @@ public static class AutoBindingOptionsBuilderExtensions
{
/// <summary>
/// Automatically binds an instance of <typeparamref name="TOptions" /> to data in configuration providers and adds it
/// to the DI container if the
/// type hasn't already been registered.
/// to the DI container if the type hasn't already been registered.
/// </summary>
/// <typeparam name="TOptions">The type of options being configured.</typeparam>
/// <param name="builder">The <see cref="T:Microsoft.Extensions.Options.OptionsBuilder`1" /> instance.</param>
/// <param name="suffix">
/// The suffix to be removed from the type name when binding <typeparamref name="TOptions" /> to the
/// configuration instance.
/// <param name="keys">
/// The list of keys to match when binding <typeparamref name="TOptions" /> to the configuration
/// instance.
/// </param>
/// <returns>The <see cref="T:Microsoft.Extensions.Options.OptionsBuilder`1" />.</returns>
public static OptionsBuilder<TOptions> AutoBind<TOptions>(this OptionsBuilder<TOptions> builder,
string suffix = Constants.DefaultOptionsSuffix)
params string[] keys)
where TOptions : class
{
builder = builder ?? throw new ArgumentNullException(nameof(builder));

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

builder.Services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(provider =>
var attribute = typeof(TOptions).GetCustomAttribute<AutoBindAttribute>();
if (keys.Length > 0)
{
match.AddRange(keys);
}
else if (attribute != null && attribute.Keys.Length > 0)
{
match.AddRange(attribute.Keys);
}
else
{
var name = typeof(TOptions).Name;
match.Add(name);

if (name.EndsWith(Constants.DefaultOptionsSuffix))
{
match.Add(name.Remove(name.Length - Constants.DefaultOptionsSuffix.Length));
}
else
{
match.Add($"{name}{Constants.DefaultOptionsSuffix}");
}
}

builder.Configure<IConfiguration>((option, configuration) => configuration.TryBind(option, match, out _));

builder.Services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsChangeTokenSource<TOptions>), provider =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
return new ConfigurationChangeTokenSource<TOptions>(configuration);
});
}));

builder.Services.AddSingleton(provider =>
builder.Services.TryAdd(ServiceDescriptor.Singleton(typeof(TOptions), provider =>
{
var options = provider.GetRequiredService<IOptions<TOptions>>();
return options.Value;
});
}));

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace Extensions.Options.AutoBinder
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods for automatically binding strongly typed options to data in configuration providers.
/// </summary>
public static class AutoBindingServiceCollectionExtensions
{
/// <summary>
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AutoBindOptions(this IServiceCollection services)
{
return AutoBindOptions(services, Assembly.GetCallingAssembly());
}

/// <summary>
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
/// <param name="markerType">Marker type of the assembly to scan.</param>
/// <param name="additionalTypes">Additional marker types of the assemblies to scan.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AutoBindOptions(this IServiceCollection services, Type markerType, params
Type[] additionalTypes)
{
return AutoBindOptions(services, markerType.Assembly,
additionalTypes.Select(type => type.Assembly).ToArray());
}

/// <summary>
/// Registers and binds strongly typed objects from the specified assemblies to data in configuration providers.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> instance.</param>
/// <param name="assembly">Assembly to scan.</param>
/// <param name="additionalAssemblies">Additional assemblies to scan.</param>
/// <returns>The <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AutoBindOptions(this IServiceCollection services, Assembly assembly, params
Assembly[] additionalAssemblies)
{
_ = services ?? throw new ArgumentNullException(nameof(services));
services.AddOptions();

var assemblies = additionalAssemblies.Prepend(assembly).Distinct();
var types = assemblies.SelectMany(GetTypesWithAttribute<AutoBindAttribute>);

var optionsMethod = typeof(OptionsServiceCollectionExtensions).GetMethods().Single(
methodInfo =>
methodInfo.Name == nameof(OptionsServiceCollectionExtensions.AddOptions) &&
methodInfo.GetGenericArguments().Length == 1 &&
methodInfo.GetParameters().Length == 1 &&
methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection));

var binderMethod = typeof(AutoBindingOptionsBuilderExtensions).GetMethod(
nameof(AutoBindingOptionsBuilderExtensions.AutoBind),
BindingFlags.Static | BindingFlags.Public);

foreach (var optionsType in types)
{
var genericOptionsMethod = optionsMethod.MakeGenericMethod(optionsType);
var builder = genericOptionsMethod.Invoke(null, new object[] { services });
var genericBinderMethod = binderMethod!.MakeGenericMethod(optionsType);
genericBinderMethod.Invoke(null, new[] { builder, new string[] { } });
}

return services;
}

private static IEnumerable<Type> GetTypesWithAttribute<TAttribute>(Assembly assembly)
{
return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(TAttribute), true).Length > 0);
}
}
}
Loading