Skip to content

Commit ca1fdb1

Browse files
snakefoot304NotModified
authored andcommitted
NLog.Extensions.Configuration - ConfigSetting LayoutRenderer (#245)
1 parent b37f5ac commit ca1fdb1

File tree

11 files changed

+423
-1
lines changed

11 files changed

+423
-1
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ install:
1818
# Restore dependencies
1919
- dotnet restore src/NLog.Extensions.Logging
2020
- dotnet restore src/NLog.Extensions.Hosting
21+
- dotnet restore src/NLog.Extensions.Configuration
2122
- dotnet restore test/NLog.Extensions.Hosting.Tests
2223
- dotnet restore test/NLog.Extensions.Logging.Tests
24+
- dotnet restore test/NLog.Extensions.Configuration.Tests
2325

2426
script:
2527
# Run tests
2628
- dotnet test test/NLog.Extensions.Hosting.Tests --configuration Release --framework netcoreapp2.0
2729
- dotnet test test/NLog.Extensions.Logging.Tests --configuration Release --framework netcoreapp2.0
30+
- dotnet test test/NLog.Extensions.Configuration.Tests --configuration Release --framework netcoreapp2.0

NLog.Extensions.Logging.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLog.Extensions.Logging.Tes
2020
EndProject
2121
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostingExample", "examples\NetCore2\HostingExample\HostingExample.csproj", "{07D358DF-D77A-434B-B034-95785DF7106F}"
2222
EndProject
23+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLog.Extensions.Configuration", "src\NLog.Extensions.Configuration\NLog.Extensions.Configuration.csproj", "{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B}"
24+
EndProject
25+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLog.Extensions.Configuration.Tests", "test\NLog.Extensions.Configuration.Tests\NLog.Extensions.Configuration.Tests.csproj", "{78A9081B-066B-4B34-BBD7-764D53CE4AA3}"
26+
EndProject
2327
Global
2428
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2529
Debug|Any CPU = Debug|Any CPU
@@ -50,6 +54,14 @@ Global
5054
{07D358DF-D77A-434B-B034-95785DF7106F}.Debug|Any CPU.Build.0 = Debug|Any CPU
5155
{07D358DF-D77A-434B-B034-95785DF7106F}.Release|Any CPU.ActiveCfg = Release|Any CPU
5256
{07D358DF-D77A-434B-B034-95785DF7106F}.Release|Any CPU.Build.0 = Release|Any CPU
57+
{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
58+
{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
59+
{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
60+
{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B}.Release|Any CPU.Build.0 = Release|Any CPU
61+
{78A9081B-066B-4B34-BBD7-764D53CE4AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62+
{78A9081B-066B-4B34-BBD7-764D53CE4AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
63+
{78A9081B-066B-4B34-BBD7-764D53CE4AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
64+
{78A9081B-066B-4B34-BBD7-764D53CE4AA3}.Release|Any CPU.Build.0 = Release|Any CPU
5365
EndGlobalSection
5466
GlobalSection(SolutionProperties) = preSolution
5567
HideSolutionNode = FALSE
@@ -61,6 +73,8 @@ Global
6173
{0DC000BA-2DF8-48E5-A7BC-D76CB9D3FC61} = {FBD2E07B-F25B-4D2F-AEF6-6D1E10F1E523}
6274
{DC42BF57-6316-4FCA-AD33-48FFDAFB4712} = {FBD2E07B-F25B-4D2F-AEF6-6D1E10F1E523}
6375
{07D358DF-D77A-434B-B034-95785DF7106F} = {BD106966-02BE-4137-B9DC-4ECE56B4C204}
76+
{AE82D026-CE85-48CC-BFFE-2D5C1556CC2B} = {C21FD102-21B1-46DB-AD62-86692558AD01}
77+
{78A9081B-066B-4B34-BBD7-764D53CE4AA3} = {FBD2E07B-F25B-4D2F-AEF6-6D1E10F1E523}
6478
EndGlobalSection
6579
GlobalSection(ExtensibilityGlobals) = postSolution
6680
SolutionGuid = {46DF0C22-7B6A-4A64-BC63-7B2F6A14F334}

appveyor.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ artifacts:
2323
test_script:
2424
- nuget.exe install OpenCover -ExcludeVersion -DependencyVersion Ignore
2525
- OpenCover\tools\OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -f netcoreapp1.1 NLog.Extensions.Logging.Tests" -filter:"+[NLog.Extensions.Logging]* +[NLog.Extensions.Hosting]* -[NLog.Extensions.Logging.Tests]* -[NLog.Extensions.Hosting.Tests]*" -output:"coverage.xml" -oldstyle -targetdir:"test"
26-
- OpenCover\tools\OpenCover.Console.exe -register:user -mergeoutput -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -f netcoreapp2.0 NLog.Extensions.Logging.Tests" -filter:"+[NLog.Extensions.Logging]* +[NLog.Extensions.Hosting]* -[NLog.Extensions.Logging.Tests]* -[NLog.Extensions.Hosting.Tests]*" -output:"coverage.xml" -oldstyle -targetdir:"test"
26+
- OpenCover\tools\OpenCover.Console.exe -register:user -mergeoutput -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -f netcoreapp2.0 NLog.Extensions.Logging.Tests" -filter:"+[NLog.Extensions.Logging]* -[NLog.Extensions.Logging.Tests]*" -output:"coverage.xml" -oldstyle -targetdir:"test"
2727
- OpenCover\tools\OpenCover.Console.exe -register:user -mergeoutput -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -f netcoreapp2.0 NLog.Extensions.Hosting.Tests" -filter:"+[NLog.Extensions.Logging]* +[NLog.Extensions.Hosting]* -[NLog.Extensions.Logging.Tests]* -[NLog.Extensions.Hosting.Tests]*" -output:"coverage.xml" -oldstyle -targetdir:"test"
28+
- OpenCover\tools\OpenCover.Console.exe -register:user -mergeoutput -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -f netcoreapp2.0 NLog.Extensions.Configuration.Tests" -filter:"+[NLog.Extensions.Logging]* +[NLog.Extensions.Hosting]* +[NLog.Extensions.Configuration]* -[NLog.Extensions.Logging.Tests]* -[NLog.Extensions.Hosting.Tests]* -[NLog.Extensions.Configuration.Tests]*" -output:"coverage.xml" -oldstyle -targetdir:"test"
2829
- pip install codecov
2930
- codecov -f "coverage.xml"
3031
- ps: .\run-sonar.ps1

build.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ dotnet restore .\src\NLog.Extensions.Hosting\
1717
if (-Not $LastExitCode -eq 0)
1818
{ exit $LastExitCode }
1919

20+
dotnet restore .\src\NLog.Extensions.Configuration\
21+
if (-Not $LastExitCode -eq 0)
22+
{ exit $LastExitCode }
23+
2024
msbuild /t:Pack .\src\NLog.Extensions.Logging\ /p:targetFrameworks='"net451;net461;netstandard1.3;netstandard1.5;netstandard2.0"' /p:VersionPrefix=$versionPrefix /p:VersionSuffix=$versionSuffix /p:FileVersion=$versionFile /p:ProductVersion=$versionProduct /p:Configuration=Release /p:IncludeSymbols=true /p:PackageOutputPath=..\..\artifacts /verbosity:minimal
2125
if (-Not $LastExitCode -eq 0)
2226
{ exit $LastExitCode }
@@ -25,4 +29,9 @@ msbuild /t:Pack .\src\NLog.Extensions.Hosting\ /p:targetFrameworks='"netstandard
2529
if (-Not $LastExitCode -eq 0)
2630
{ exit $LastExitCode }
2731

32+
msbuild /t:Pack .\src\NLog.Extensions.Configuration\ /p:targetFrameworks='"net451;net461;netstandard1.3;netstandard1.5;netstandard2.0"' /p:VersionPrefix=$versionPrefix /p:VersionSuffix=$versionSuffix /p:FileVersion=$versionFile /p:ProductVersion=$versionProduct /p:Configuration=Release /p:IncludeSymbols=true /p:PackageOutputPath=..\..\artifacts /verbosity:minimal
33+
if (-Not $LastExitCode -eq 0)
34+
{ exit $LastExitCode }
35+
36+
2837
exit $LastExitCode
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.Extensions.Configuration;
4+
using NLog.Config;
5+
using NLog.Layouts;
6+
using NLog.LayoutRenderers;
7+
using System.Text;
8+
9+
namespace NLog.Extensions.Configuration
10+
{
11+
/// <summary>
12+
/// Layout renderer that can lookup values from Microsoft Extension Configuration Container (json, xml, ini)
13+
/// </summary>
14+
/// <remarks>Not to be confused with NLog.AppConfig that includes ${appsetting}</remarks>
15+
/// <example>
16+
/// Example: appsettings.json
17+
/// {
18+
/// "Mode":"Prod",
19+
/// "Options":{
20+
/// "StorageConnectionString":"UseDevelopmentStorage=true",
21+
/// }
22+
/// }
23+
///
24+
/// Config Setting Lookup:
25+
/// ${configsetting:name=Mode} = "Prod"
26+
/// ${configsetting:name=Options.StorageConnectionString} = "UseDevelopmentStorage=true"
27+
/// ${configsetting:name=Options.TableName:default=MyTable} = "MyTable"
28+
///
29+
/// Config Setting Lookup Cached:
30+
/// ${configsetting:cached=True:name=Mode}
31+
/// </example>
32+
[LayoutRenderer("configsetting")]
33+
public class ConfigSettingLayoutRenderer : LayoutRenderer
34+
{
35+
internal IConfigurationRoot _configurationRoot;
36+
37+
private static readonly Dictionary<string, WeakReference<IConfigurationRoot>> _cachedConfigFiles = new Dictionary<string, WeakReference<IConfigurationRoot>>();
38+
39+
/// <summary>
40+
/// Global Configuration Container. Used if <see cref="FileName" /> has default value
41+
/// </summary>
42+
public static IConfiguration DefaultConfiguration { get; set; }
43+
44+
///<summary>
45+
/// Name of the setting
46+
///</summary>
47+
[RequiredParameter]
48+
[DefaultParameter]
49+
public string Name { get => _name; set => _name = value?.Replace(".", ":"); }
50+
private string _name;
51+
52+
///<summary>
53+
/// The default value to render if the setting value is null.
54+
///</summary>
55+
public string Default { get; set; }
56+
57+
/// <summary>
58+
/// Configuration FileName (Multiple filenames can be split using '|' pipe-character)
59+
/// </summary>
60+
/// <remarks>Relative paths are automatically prefixed with ${basedir}</remarks>
61+
public Layout FileName { get; set; } = DefaultFileName;
62+
private const string DefaultFileName = "appsettings.json|appsettings.${environment:variable=ASPNETCORE_ENVIRONMENT}.json";
63+
64+
/// <inheritdoc/>
65+
protected override void InitializeLayoutRenderer()
66+
{
67+
_configurationRoot = null;
68+
base.InitializeLayoutRenderer();
69+
}
70+
71+
/// <inheritdoc/>
72+
protected override void CloseLayoutRenderer()
73+
{
74+
_configurationRoot = null;
75+
base.CloseLayoutRenderer();
76+
}
77+
78+
/// <inheritdoc/>
79+
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
80+
{
81+
if (string.IsNullOrEmpty(_name))
82+
return;
83+
84+
string value = null;
85+
var configurationRoot = TryGetConfigurationRoot();
86+
if (configurationRoot != null)
87+
{
88+
value = configurationRoot[_name];
89+
}
90+
91+
builder.Append(value ?? Default);
92+
}
93+
94+
private IConfiguration TryGetConfigurationRoot()
95+
{
96+
if (DefaultConfiguration != null)
97+
{
98+
var simpleLayout = FileName as SimpleLayout;
99+
if (simpleLayout == null || string.IsNullOrEmpty(simpleLayout.Text) || ReferenceEquals(simpleLayout.Text, DefaultFileName))
100+
{
101+
if (_configurationRoot != null)
102+
_configurationRoot = null;
103+
return DefaultConfiguration;
104+
}
105+
}
106+
107+
if (_configurationRoot != null)
108+
return _configurationRoot;
109+
110+
var fileNames = FileName?.Render(LogEventInfo.CreateNullEvent());
111+
if (!string.IsNullOrEmpty(fileNames))
112+
{
113+
return _configurationRoot = LoadFileConfiguration(fileNames);
114+
}
115+
116+
return null;
117+
}
118+
119+
private IConfigurationRoot LoadFileConfiguration(string fileNames)
120+
{
121+
lock (_cachedConfigFiles)
122+
{
123+
if (_cachedConfigFiles.TryGetValue(fileNames, out var wearkConfigRoot) && wearkConfigRoot.TryGetTarget(out var configRoot))
124+
{
125+
return configRoot;
126+
}
127+
else
128+
{
129+
configRoot = BuildConfigurationRoot(fileNames);
130+
_cachedConfigFiles[fileNames] = new WeakReference<IConfigurationRoot>(configRoot);
131+
return configRoot;
132+
}
133+
}
134+
}
135+
136+
private static IConfigurationRoot BuildConfigurationRoot(string fileNames)
137+
{
138+
var configBuilder = new ConfigurationBuilder();
139+
string baseDir = null;
140+
foreach (var fileName in fileNames.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
141+
{
142+
var fullPath = fileName;
143+
if (!System.IO.Path.IsPathRooted(fullPath))
144+
{
145+
if (baseDir == null)
146+
baseDir = new BaseDirLayoutRenderer().Render(LogEventInfo.CreateNullEvent()) ?? string.Empty;
147+
fullPath = System.IO.Path.Combine(baseDir, fileName);
148+
}
149+
150+
AddFileConfiguration(configBuilder, fullPath);
151+
}
152+
153+
return configBuilder.Build();
154+
}
155+
156+
private static void AddFileConfiguration(ConfigurationBuilder configBuilder, string fullPath)
157+
{
158+
if (System.IO.File.Exists(fullPath))
159+
{
160+
// NOTE! Decided not to monitor for changes, as it would require access to dipose this monitoring again
161+
if (string.Equals(System.IO.Path.GetExtension(fullPath), ".json", StringComparison.OrdinalIgnoreCase))
162+
{
163+
configBuilder.AddJsonFile(fullPath);
164+
}
165+
else if (string.Equals(System.IO.Path.GetExtension(fullPath), ".xml", StringComparison.OrdinalIgnoreCase))
166+
{
167+
configBuilder.AddXmlFile(fullPath);
168+
}
169+
else if (string.Equals(System.IO.Path.GetExtension(fullPath), ".ini", StringComparison.OrdinalIgnoreCase))
170+
{
171+
configBuilder.AddIniFile(fullPath);
172+
}
173+
else
174+
{
175+
Common.InternalLogger.Info("configSetting - Skipping FileName with unknown file-extension: {0}", fullPath);
176+
}
177+
}
178+
else
179+
{
180+
Common.InternalLogger.Info("configSetting - Skipping FileName as file doesnt't exists: {0}", fullPath);
181+
}
182+
}
183+
}
184+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net451;net461;netstandard1.3;netstandard1.5;netstandard2.0</TargetFrameworks>
5+
<DebugType>full</DebugType>
6+
<DebugSymbols>true</DebugSymbols>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
10+
<Product>NLog.Extensions.Configuration v$(ProductVersion)</Product>
11+
<InformationalVersion>$(ProductVersion)</InformationalVersion>
12+
13+
<Company>NLog</Company>
14+
<Authors>Julian Verdurmen;CoCo Lin</Authors>
15+
<Description>NLog extensions for Microsoft.Extensions.Configuration</Description>
16+
<PackageProjectUrl>https://github.com/NLog/NLog.Extensions.Logging</PackageProjectUrl>
17+
<PackageLicenseUrl>https://github.com/NLog/NLog.Extensions.Logging/blob/master/LICENSE</PackageLicenseUrl>
18+
<PackageIconUrl>https://nlog-project.org/NConfig.png</PackageIconUrl>
19+
<RepositoryUrl>https://github.com/NLog/NLog.Extensions.Logging.git</RepositoryUrl>
20+
<RepositoryType>git</RepositoryType>
21+
<PackageTags>NLog;Microsoft.Extensions.Configuration;log;logfiles;netcore</PackageTags>
22+
<PackageReleaseNotes>1.0: Initial release</PackageReleaseNotes>
23+
24+
<!-- SonarQube needs this -->
25+
<ProjectGuid>{0A5EC30A-2DC6-4EB3-BF1E-2E82BA83220F}</ProjectGuid>
26+
<SignAssembly>true</SignAssembly>
27+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
28+
<AssemblyOriginatorKeyFile>..\NLog.snk</AssemblyOriginatorKeyFile>
29+
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
30+
</PropertyGroup>
31+
32+
<PropertyGroup Condition=" '$(TargetFramework)' == 'net461' ">
33+
<Title>NLog.Extensions.Configuration for .NET Framework 4.6.1</Title>
34+
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
35+
</PropertyGroup>
36+
<PropertyGroup Condition=" '$(TargetFramework)' == 'net451' ">
37+
<Title>NLog.Extensions.Configuration for .NET Framework 4.5.1</Title>
38+
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
39+
</PropertyGroup>
40+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
41+
<Title>NLog.Extensions.Configuration for NetStandard 1.3</Title>
42+
</PropertyGroup>
43+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.5' ">
44+
<Title>NLog.Extensions.Configuration for NetStandard 1.5</Title>
45+
</PropertyGroup>
46+
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
47+
<Title>NLog.Extensions.v for NetStandard 2.0</Title>
48+
</PropertyGroup>
49+
50+
<ItemGroup>
51+
<PackageReference Include="NLog" Version="4.5.9" />
52+
</ItemGroup>
53+
54+
<ItemGroup Condition=" '$(TargetFramework)' == 'net451' ">
55+
<Reference Include="System" />
56+
<Reference Include="Microsoft.CSharp" />
57+
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="1.1.0" />
58+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.0" />
59+
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="1.1.0" />
60+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.0" />
61+
</ItemGroup>
62+
63+
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
64+
<Reference Include="System" />
65+
<Reference Include="Microsoft.CSharp" />
66+
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
67+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
68+
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="2.0.0" />
69+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
70+
</ItemGroup>
71+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
72+
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="1.1.0" />
73+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.0" />
74+
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="1.1.0" />
75+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.0" />
76+
</ItemGroup>
77+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.5' ">
78+
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="1.1.0" />
79+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.0" />
80+
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="1.1.0" />
81+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.0" />
82+
</ItemGroup>
83+
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
84+
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="2.0.0" />
85+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
86+
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="2.0.0" />
87+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
88+
</ItemGroup>
89+
</Project>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using System.Runtime.CompilerServices;
2+
using System.Runtime.InteropServices;
3+
4+
[assembly: ComVisible(false)]
5+
6+
[assembly: InternalsVisibleTo("NLog.Extensions.Configuration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100772391E63C104728ADCF18E2390474262559FA7F34A4215848F43288CDE875DCC92A06222E9BE0592B211FF74ADBB5D21A7AAB5522B540B1735F2F03279221056FEDBE7E534073DABEE9DB48F8ECEBCF1DC98A95576E45CBEFF5FE7C4842859451AB2DAE7A8370F1B2F7A529D2CA210E3E844D973523D73D193DF6C17F1314A6")]

0 commit comments

Comments
 (0)