Description
openedon Apr 25, 2018
When configuration is loaded from both a JSON and an XML file, the configuration may be invalid depending on the order in which the files are added to the ConfigurationBuilder
Functional impact
In our test scenario, Serilog configuration failed to load properly because of this bug.
Minimal repro steps
- Create a .NET Core 2.0 console application with an appsettings.xml and an appsettings.json file
- Fill in the appsettings.xml file like this:
<settings>
<Serilog>
<WriteTo Name="0">
<Args>
<pathFormat>C:\FooBar\Logs</pathFormat>
</Args>
</WriteTo>
</Serilog>
</settings>
- Fill in the appsettings.json like this:
{
"Serilog": {
"WriteTo": [
{
"Name": "RollingFile"
}
]
}
}
- Configure both files with "Build Action" set to "Content"
- Update Program.cs to the following:
class Program
{
static void Main(string[] args)
{
var jsonConfiguration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
var xmlConfiguration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("appsettings.xml", optional: false)
.Build();
Console.WriteLine("*** JSON CONFIG ***");
foreach (var (key, value) in jsonConfiguration.AsEnumerable())
{
Console.WriteLine($"{key, -25} = {value}");
}
Console.WriteLine();
Console.WriteLine("*** XML CONFIG ***");
foreach (var (key, value) in xmlConfiguration.AsEnumerable())
{
Console.WriteLine($"{key, -25} = {value}");
}
Console.WriteLine();
Console.ReadLine();
}
}
- Add the following NuGet references to your project
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="2.0.1" />
</ItemGroup>
- Startup the application
Expected result
The XML config should NOT produce a 'Name' key in the configuration
I would expect the following output:
*** JSON CONFIG ***
Serilog =
Serilog:WriteTo =
Serilog:WriteTo:0 =
Serilog:WriteTo:0:Name = RollingFile
*** XML CONFIG ***
Serilog =
Serilog:WriteTo =
Serilog:WriteTo:0 =
Serilog:WriteTo:0:Args =
Serilog:WriteTo:0:Args:pathFormat = C:\FooBar\Logs
Actual result
The XML config produces a 'Name' key in the configuration
*** JSON CONFIG ***
Serilog =
Serilog:WriteTo =
Serilog:WriteTo:0 =
Serilog:WriteTo:0:Name = RollingFile
*** XML CONFIG ***
Serilog =
Serilog:WriteTo =
Serilog:WriteTo:0 =
Serilog:WriteTo:0:Name = 0
Serilog:WriteTo:0:Args =
Serilog:WriteTo:0:Args:pathFormat = C:\FooBar\Logs
Impact
As you can see, both the JSON and the XML file produce a key 'Serilog:WriteTo:0:Name', once with 0 and once with RollingFile. Due to the way the ConfigurationBuilder works, the last key will win.
When the value of Serilog:WriteTo:0:Name is 'RollingFile', Serilog will successfully configure the sink and configuration will succeed. When the value is '0' however, Serilog will skip configuring this sink.
This means that when the configuration builder is called like follows:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddXmlFile("appsettings.xml", optional: false)
.Build();
It will not work, because the key Serilog:WriteTo:0:Name from the XML file wins, breaking Serilog.
If you change it to this however:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddXmlFile("appsettings.xml", optional: false)
.AddJsonFile("appsettings.json", optional: false)
.Build();
The program will correctly configure Serilog.
Further information
This issue is actually closely related to another issue I've created here: dotnet/extensions#745
The proposed solution there also solves this issue, as it removes the need to use "Name" attributes to simulate arrays in XML and thus avoids this subtle problem.