Skip to content

Commit

Permalink
Merge pull request #5 from gsoft-inc/feature/cached-configuration-sou…
Browse files Browse the repository at this point in the history
…rces

Caching configuration sources to prevent duplicating configuration providers
  • Loading branch information
asimmon authored Aug 2, 2022
2 parents b2a95ee + 6b584ef commit a88bc57
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 247 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json");
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddSubstitution(); // <-- Add this after other configuration providers
// Setup your services
// [...]
builder.Services.AddSubstitution(); // <-- Add also this to your service collection
```


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Diagnostics;
using Microsoft.Extensions.Configuration;

namespace GSoft.Extensions.Configuration.Substitution;

[DebuggerDisplay("CachedConfigurationSource({_underlyingConfigurationSourceType})")]
internal sealed class CachedConfigurationSource : IConfigurationSource
{
private readonly object _lockObject = new object();
private readonly IConfigurationSource _underlyingConfigurationSource;
private readonly string _underlyingConfigurationSourceType;
private volatile IConfigurationProvider? _configurationProvider;

public CachedConfigurationSource(IConfigurationSource underlyingConfigurationSource)
{
this._underlyingConfigurationSource = underlyingConfigurationSource ?? throw new ArgumentNullException(nameof(underlyingConfigurationSource));
this._underlyingConfigurationSourceType = this._underlyingConfigurationSource.GetType().Name;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)
{
if (this._configurationProvider != null)
{
return this._configurationProvider;
}

lock (this._lockObject)
{
if (this._configurationProvider != null)
{
return this._configurationProvider;
}

this._configurationProvider = this._underlyingConfigurationSource.Build(builder);
}

return this._configurationProvider;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace GSoft.Extensions.Configuration.Substitution;

internal sealed class ChainedSubstitutedConfigurationProvider : ConfigurationProvider
internal sealed class ChainedSubstitutedConfigurationProvider : IConfigurationProvider
{
private readonly IConfiguration _configuration;
private readonly bool _eagerValidation;

public ChainedSubstitutedConfigurationProvider(IConfigurationRoot configuration, bool eagerValidation)
public ChainedSubstitutedConfigurationProvider(IConfiguration configuration, bool eagerValidation)
{
this.Configuration = configuration;
this._configuration = configuration;
this._eagerValidation = eagerValidation;
}

public IConfigurationRoot Configuration { get; }

public override bool TryGet(string key, out string value)
public bool TryGet(string key, out string value)
{
var substituted = ConfigurationSubstitutor.GetSubstituted(this.Configuration, key);
var substituted = ConfigurationSubstitutor.GetSubstituted(this._configuration, key);
if (substituted == null)
{
value = string.Empty;
Expand All @@ -29,9 +29,14 @@ public override bool TryGet(string key, out string value)
return true;
}

public override void Set(string key, string value) => this.Configuration[key] = value;
public void Set(string key, string value) => this._configuration[key] = value;

public IChangeToken GetReloadToken()
{
return this._configuration.GetReloadToken();
}

public override void Load()
public void Load()
{
if (this._eagerValidation)
{
Expand All @@ -41,18 +46,17 @@ public override void Load()

private void EnsureAllKeysAreSubstituted()
{
foreach (var kvp in this.Configuration.AsEnumerable())
foreach (var kvp in this._configuration.AsEnumerable())
{
// This loop goes through the entire configuration (even nested sections).
// Reading each individual value triggers the substitution process and it will throw if a referenced key is unresolved.
_ = kvp.Value;
}
}

public override IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
{
IConfiguration config = this.Configuration;
var section = parentPath == null ? config : config.GetSection(parentPath);
var section = parentPath == null ? this._configuration : this._configuration.GetSection(parentPath);
var keys = section.GetChildren().Select(c => c.Key);
return keys.Concat(earlierKeys).OrderBy(k => k, ConfigurationKeyComparer.Instance).ToArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ namespace GSoft.Extensions.Configuration.Substitution;

internal sealed class ChainedSubstitutedConfigurationSource : IConfigurationSource
{
private readonly IConfigurationSource[] _configurationSources;
private readonly bool _eagerValidation;

public ChainedSubstitutedConfigurationSource(IConfigurationSource[] configurationSources, bool eagerValidation)
public ChainedSubstitutedConfigurationSource(bool eagerValidation)
{
this._configurationSources = configurationSources;
this._eagerValidation = eagerValidation;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)
{
var configurationBuilder = new ConfigurationBuilder();

foreach (var configurationSource in this._configurationSources)
for (var i = 0; i < builder.Sources.Count && builder.Sources[i] is not ChainedSubstitutedConfigurationSource; i++)
{
configurationBuilder.Add(configurationSource);
configurationBuilder.Add(builder.Sources[i]);
}

return new ChainedSubstitutedConfigurationProvider(configurationBuilder.Build(), this._eagerValidation);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using GSoft.Extensions.Configuration.Substitution;

// ReSharper disable once CheckNamespace
Expand All @@ -17,8 +16,15 @@ public static class ConfigurationSubstitutorBuilderExtensions
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddSubstitution(this IConfigurationBuilder configurationBuilder, bool eagerValidation = false)
{
// Filtering the sources prevents infinite recursive loop where several instances of our configuration source type would wrap each other
var configurationSources = configurationBuilder.Sources.Where(x => x is not ChainedSubstitutedConfigurationSource).ToArray();
return configurationBuilder.Add(new ChainedSubstitutedConfigurationSource(configurationSources, eagerValidation));
for (var i = 0; i < configurationBuilder.Sources.Count; i++)
{
var configurationSource = configurationBuilder.Sources[i];
if (configurationSource is not CachedConfigurationSource)
{
configurationBuilder.Sources[i] = new CachedConfigurationSource(configurationSource);
}
}

return configurationBuilder.Add(new ChainedSubstitutedConfigurationSource(eagerValidation));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit a88bc57

Please sign in to comment.