Skip to content
Open
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# tinyconfig
TinyConfig is a simple library to build custom configuration provider in .NET with reload support.
# TinyConfig

TinyConfig is a simple library to democratize building custom configuration provider in .NET.
18 changes: 18 additions & 0 deletions TinyConfig.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.6.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
26 changes: 26 additions & 0 deletions src/TinyConfig.Abstractions/ITinyConfigOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

namespace TinyConfig.Abstractions
{
/// <summary>
/// tiny config options
/// </summary>
public interface ITinyConfigOptions
{
/// <summary>
/// indicate whether tiny config is enabled, false by default
/// </summary>
bool Enabled { get => false; }

/// <summary>
/// indicate whether configuration should be reloaded when a change occurs
/// Has default value "false"
/// </summary>
bool ReloadOnChange { get => false; }

/// <summary>
/// number of seconds to wait before reloading after a change
/// Has default value "60" seconds
/// </summary>
int ReloadDelay { get => 60; }
}
}
24 changes: 24 additions & 0 deletions src/TinyConfig.Abstractions/ITinyConfigStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TinyConfig.Abstractions
{
/// <summary>
/// wraps a configuration store, providing access to the actual configuration values
/// </summary>
public interface ITinyConfigStore
{
/// <summary>
/// check if a change has occurred on the configuration store
/// </summary>
/// <param name="versionToken"></param>
/// <returns>true is settings has changed</returns>
Task<bool> HasChanged(object versionToken = null);

/// <summary>
/// get all settings from configuration store
/// </summary>
/// <returns>a list containing all configuration key value pairs, with table version token</returns>
Task<(IEnumerable<ITinySetting>, object)> GetAllWithVersionToken();
}
}
24 changes: 24 additions & 0 deletions src/TinyConfig.Abstractions/ITinySetting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

namespace TinyConfig.Abstractions
{
/// <summary>
/// settings model
/// </summary>
public interface ITinySetting
{
/// <summary>
/// settings id (key)
/// </summary>
string Id { get; }

/// <summary>
/// settings value
/// </summary>
string Value { get; }

/// <summary>
/// settings version
/// </summary>
byte[] Version { get; }
}
}
7 changes: 7 additions & 0 deletions src/TinyConfig.Abstractions/TinyConfig.Abstractions.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

</Project>
38 changes: 38 additions & 0 deletions src/TinyConfig.Core/Extensions/ConfigurationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TinyConfig.Abstractions;

namespace TinyConfig.Core.Extensions
{
/// <summary>
/// configuration builder extensions
/// </summary>
public static class ConfigurationBuilderExtensions
{
private const string ENABLED_FLAG = "TinyConfig:Enabled";
private const string CONFIG_SECTION = "TinyConfig";

/// <summary>
/// add db configuration to list of config
/// </summary>
/// <param name="builder"></param>
/// <param name="configuration">existing configuration value</param>
/// <param name="logger">logger instance</param>
/// <returns><see cref="IConfigurationBuilder" /></returns>
public static IConfigurationBuilder AddTinyConfig<TOptions, TStore>(
this IConfigurationBuilder builder, IConfiguration configuration,
ILogger logger = null)
where TOptions : class, ITinyConfigOptions, new()
where TStore : class, ITinyConfigStore
{
if (configuration.GetValue<bool>(ENABLED_FLAG))
{
var config = new TOptions();
configuration.Bind(CONFIG_SECTION, config);
return builder.Add(new TinyConfigSource<TOptions, TStore>(config, logger));
}

return builder;
}
}
}
27 changes: 27 additions & 0 deletions src/TinyConfig.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TinyConfig.Abstractions;

namespace TinyConfig.Core.Extensions
{
/// <summary>
/// service collection extensions
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// add tiny configuration reloader
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <typeparam name="TOptions">concrete implementation for config</typeparam>
/// <typeparam name="TStore">concrete implementation for store</typeparam>
/// <returns><see cref="IServiceCollection"/></returns>
public static IServiceCollection AddTinyConfigReloader<TOptions, TStore>(this IServiceCollection services,
IConfiguration configuration)
where TOptions : class, ITinyConfigOptions, new()
where TStore : class, ITinyConfigStore =>
services.AddSingleton((IConfigurationRoot) configuration)
.AddHostedService<TinyConfigChangeWatcher<TOptions, TStore>>();
}
}
16 changes: 16 additions & 0 deletions src/TinyConfig.Core/TinyConfig.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TinyConfig.Abstractions\TinyConfig.Abstractions.csproj" />
</ItemGroup>

</Project>
81 changes: 81 additions & 0 deletions src/TinyConfig.Core/TinyConfigChangeWatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TinyConfig.Abstractions;

namespace TinyConfig.Core
{
/// <summary>
/// Background service to notify about configuration data changes.
/// </summary>
public class TinyConfigChangeWatcher<TOptions, TStore> : BackgroundService
where TOptions : class, ITinyConfigOptions
where TStore : class, ITinyConfigStore
{
private readonly ILogger<TinyConfigChangeWatcher<TOptions, TStore>> _logger;
private readonly IEnumerable<TinyConfigProvider<TOptions, TStore>> _configProviders;

/// <summary>
/// Initializes a new instance of the <see cref="DbConfigChangeWatcher"/> class.
/// test.
/// </summary>
/// <param name="configurationRoot" />
/// <param name="logger" />
public TinyConfigChangeWatcher(IConfigurationRoot configurationRoot, ILogger<TinyConfigChangeWatcher<TOptions, TStore>> logger)
{
if (configurationRoot == null)
{
throw new ArgumentNullException(nameof(configurationRoot));
}

_logger = logger;
_configProviders = configurationRoot.Providers.OfType<TinyConfigProvider<TOptions, TStore>>().Where(p => p.ConfigurationSource.Options.ReloadOnChange).ToList() !;
}

/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var timers = new Dictionary<int, int>(); // key - index of config provider, value - timer
var minTime = 6000;
var i = 0;
foreach (var provider in _configProviders)
{
var waitForSec = provider.ConfigurationSource.Options.ReloadDelay;
minTime = Math.Min(minTime, waitForSec);
timers[i] = waitForSec;
i++;
}

_logger.LogInformation($"DbConfigChangeWatcher will use {minTime} seconds interval");

while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(minTime), stoppingToken).ConfigureAwait(false);
if (stoppingToken.IsCancellationRequested)
{
break;
}

for (var j = 0; j < _configProviders.Count(); j++)
{
var timer = timers[j];
timer -= minTime;
if (timer <= 0)
{
_configProviders.ElementAt(j).Load();
timers[j] = _configProviders.ElementAt(j).ConfigurationSource.Options.ReloadDelay;
}
else
{
timers[j] = timer;
}
}
}
}
}
}
94 changes: 94 additions & 0 deletions src/TinyConfig.Core/TinyConfigProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TinyConfig.Abstractions;

namespace TinyConfig.Core
{
/// <summary>
/// tiny configuration provider
/// </summary>
/// <typeparam name="TOptions">concrete implementation of configuration option</typeparam>
/// <typeparam name="TStore">concrete implementation of configuration store</typeparam>
public class TinyConfigProvider<TOptions, TStore> : ConfigurationProvider
where TOptions : class, ITinyConfigOptions
where TStore : class, ITinyConfigStore
{
/// <summary>
/// configuration source
/// </summary>
public readonly TinyConfigSource<TOptions, TStore> ConfigurationSource;
private readonly ITinyConfigStore _store;
private readonly ILogger _logger;
private readonly Dictionary<string, byte[]> _versionsCache = new Dictionary<string, byte[]>();
private object VersionToken;

/// <summary>
/// ctor
/// </summary>
/// <param name="store"></param>
/// <param name="source"></param>
/// <param name="logger"></param>
public TinyConfigProvider(ITinyConfigStore store,
TinyConfigSource<TOptions, TStore> source,
ILogger logger = null)
{
ConfigurationSource = source;
_store = store;
_logger = logger;
}

/// <summary>
/// load configuration from database
/// </summary>
public override void Load()
{
var anyCheck = _store.HasChanged(VersionToken);
anyCheck.Wait();
if (anyCheck.Result)
{
_logger?.LogDebug("loading configuration from store: {0}", typeof(TStore));
var reload = LoadDatabaseConfigs();
_logger?.LogDebug("loaded configuration from store: {0}", typeof(TStore));
if (reload) OnReload();
}
}

/// <summary>
/// reload config data
/// </summary>
public bool LoadDatabaseConfigs()
{
var reload = false;
var allConfigs = _store.GetAllWithVersionToken();
allConfigs.Wait();

var (settings, lastVersion) = allConfigs.Result;

foreach (var config in settings)
{
if (!_versionsCache.ContainsKey(config.Id))
{
Set(config.Id, config.Value);
_versionsCache[config.Id] = config.Version;
reload = true;
} else
{
if (_versionsCache.TryGetValue(config.Id, out byte[] version)
&& !version.SequenceEqual(config.Version))
{
Set(config.Id, config.Value);
_versionsCache[config.Id] = config.Version;
reload = true;
}
}
}

VersionToken = lastVersion;

return reload;
}
}
}
Loading