Skip to content

Commit

Permalink
dotenv configuration provider (#223)
Browse files Browse the repository at this point in the history
* dotenv configuration provider

* Documentation
  • Loading branch information
fraliv13 authored Apr 28, 2022
1 parent 3304d62 commit 3292351
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 0 deletions.
14 changes: 14 additions & 0 deletions NBB.sln
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serilog", "Serilog", "{33AE
src\Tools\Serilog\README.md = src\Tools\Serilog\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBB.Core.Configuration", "src\Core\NBB.Core.Configuration\NBB.Core.Configuration.csproj", "{2C7CA0BD-98AD-4CC1-B496-94ED49636466}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBB.Core.Configuration.Tests", "test\UnitTests\Core\NBB.Core.Configuration.Tests\NBB.Core.Configuration.Tests.csproj", "{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -892,6 +896,14 @@ Global
{81E535EC-ACB5-4EE4-A173-06983B40F80F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81E535EC-ACB5-4EE4-A173-06983B40F80F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81E535EC-ACB5-4EE4-A173-06983B40F80F}.Release|Any CPU.Build.0 = Release|Any CPU
{2C7CA0BD-98AD-4CC1-B496-94ED49636466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2C7CA0BD-98AD-4CC1-B496-94ED49636466}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2C7CA0BD-98AD-4CC1-B496-94ED49636466}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2C7CA0BD-98AD-4CC1-B496-94ED49636466}.Release|Any CPU.Build.0 = Release|Any CPU
{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1057,6 +1069,8 @@ Global
{C964F164-C8BF-487A-9D24-5E169A7E950F} = {33AEB049-EC66-4728-9B22-CC0F282D3E73}
{81E535EC-ACB5-4EE4-A173-06983B40F80F} = {2177F9CE-CD18-4D61-8DAE-0E27D3C44AB3}
{33AEB049-EC66-4728-9B22-CC0F282D3E73} = {EF374BA2-61FC-41FB-8229-5187DCBAA63A}
{2C7CA0BD-98AD-4CC1-B496-94ED49636466} = {14726095-DA28-43A6-A9A9-F16C605932E1}
{2A91E54D-F63C-4E92-9A6D-F15C7C55B0D7} = {29B7593C-60F4-41DC-A883-4976FF467927}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23A42379-616A-43EF-99BC-803DF151F54E}
Expand Down
90 changes: 90 additions & 0 deletions src/Core/NBB.Core.Configuration/DotEnvConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.FileProviders;
using NBB.Core.Configuration.DotEnv;

namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// Extension methods for adding <see cref="DotEnvConfigurationProvider"/>.
/// </summary>
public static class DotEnvConfigurationExtensions
{
/// <summary>
/// Adds the DotEnv configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDotEnvFile(this IConfigurationBuilder builder, string path)
{
return AddDotEnvFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
}

/// <summary>
/// Adds the DotEnv configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDotEnvFile(this IConfigurationBuilder builder, string path, bool optional)
{
return AddDotEnvFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false);
}

/// <summary>
/// Adds the DotEnv configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDotEnvFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
{
return AddDotEnvFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange);
}

/// <summary>
/// Adds a DotEnv configuration source to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="provider">The <see cref="IFileProvider"/> to use to access the file.</param>
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDotEnvFile(this IConfigurationBuilder builder, IFileProvider? provider, string path, bool optional, bool reloadOnChange)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Invalid DotEnv file path", nameof(path));
}

return builder.AddDotEnvFile(s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider();
});
}

/// <summary>
/// Adds a DotEnv configuration source to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="configureSource">Configures the source.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddDotEnvFile(this IConfigurationBuilder builder, Action<DotEnvConfigurationSource>? configureSource)
=> builder.Add(configureSource);

}
}
91 changes: 91 additions & 0 deletions src/Core/NBB.Core.Configuration/DotEnvConfigurationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

using Microsoft.Extensions.Configuration;

namespace NBB.Core.Configuration.DotEnv
{
/// <summary>
/// An DotEnv file based <see cref="ConfigurationProvider"/>.
/// Files are simple line structures with environment variable declarations as key-value pairs
/// </summary>
/// <examples>
/// key1=value1
/// key2 = " value2 "
/// # comment
/// </examples>
public class DotEnvConfigurationProvider : FileConfigurationProvider
{
/// <summary>
/// Initializes a new instance with the specified source.
/// </summary>
/// <param name="source">The source settings.</param>
public DotEnvConfigurationProvider(DotEnvConfigurationSource source) : base(source) { }

/// <summary>
/// Loads the DotEnv data from a stream.
/// </summary>
/// <param name="stream">The stream to read.</param>
public override void Load(Stream stream)
=> Data = Read(stream);

/// <summary>
/// Read a stream of DotEnv values into a key/value dictionary.
/// </summary>
/// <param name="stream">The stream of DotEnv data.</param>
/// <returns>The <see cref="IDictionary{String, String}"/> which was read from the stream.</returns>
private static IDictionary<string, string?> Read(Stream stream)
{
var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
using (var reader = new StreamReader(stream))
{
string sectionPrefix = string.Empty;

while (reader.Peek() != -1)
{
string rawLine = reader.ReadLine()!;
string line = rawLine.Trim();

// Ignore blank lines
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
// Ignore comments
if (line[0] is '#')
{
continue;
}

// key = value OR "value"
int separator = line.IndexOf('=');
if (separator < 0)
{
throw new FormatException($"Unrecognized line formmat: {rawLine}");
}

string key = sectionPrefix + line.Substring(0, separator).Trim();
string value = line.Substring(separator + 1).Trim();

// Remove quotes
if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')
{
value = value.Substring(1, value.Length - 2);
}

if (data.ContainsKey(key))
{
throw new FormatException($"Duplicated key: {key}");
}

var normalizedKey = Normalize(key);
data[normalizedKey] = value;
}
}
return data;
}

private static string Normalize(string key)
=> key.Replace("__", ConfigurationPath.KeyDelimiter);
}
}
30 changes: 30 additions & 0 deletions src/Core/NBB.Core.Configuration/DotEnvConfigurationSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

using Microsoft.Extensions.Configuration;

namespace NBB.Core.Configuration.DotEnv
{
/// <summary>
/// Represents a DotEnv file as an <see cref="IConfigurationSource"/>.
/// Files are simple line structures with environment variable declarations as key-value pairs
/// </summary>
/// <examples>
/// key1=value1
/// key2 = " value2 "
/// # comment
/// </examples>
public class DotEnvConfigurationSource : FileConfigurationSource
{
/// <summary>
/// Builds the <see cref="IniConfigurationProvider"/> for this source.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
/// <returns>An <see cref="IniConfigurationProvider"/></returns>
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new DotEnvConfigurationProvider(this);
}
}
}
16 changes: 16 additions & 0 deletions src/Core/NBB.Core.Configuration/NBB.Core.Configuration.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftExtensionsPackagesVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftExtensionsPackagesVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="$(MicrosoftExtensionsPackagesVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="$(MicrosoftExtensionsPackagesVersion)" />
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions src/Core/NBB.Core.Configuration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# NBB.Core.Configuration

This package contains configuration extensions.

## NuGet install
```
dotnet add package NBB.Core.Configuration
```

## DotEnv configuration provider
Adds support for configuration files that contain environment variable declarations.

### Registration
```csharp
var builder = new ConfigurationBuilder();
builder.AddDotEnvFile("config/env.txt", optional: false, reloadOnChange: true);
```

Parameters:
* `path` - The path of the configuration file relative to the base path.
* `optional` - Whether the file is optional.
* `reloadOnChange` - Whether the configuration should be reloaded if the file changes.

### Config file example
```ini
# Serilog
Serilog__MinimumLevel__Default = Information
Serilog__MinimumLevel__Override__Microsoft = Warning

# MultiTenancy
MultiTenancy__Enabled = true
MultiTenancy__Tenants__BCR__ConnectionStrings__App_Database__User_Name = TestUser
MultiTenancy__Tenants__BCR__Description = "My description "
```

Notes:
* Each line should contain the key and the value separated by character `=`
* For hierarchical settings, the "__" separator should be used.
* Comments can be added in the file by starting the line with character `#`.
* Empty lines and whitespace around keys/values are ignored.
* Values that start or end with whitespace should be surounded by quotes. The quotes will not be part of the value.
Loading

0 comments on commit 3292351

Please sign in to comment.