Skip to content

Commit cad35a0

Browse files
committed
Improve auto-discovery of configuration assemblies in bundled mode
Neither the dependency context nor scanning DLL files on disk works with bundled (single-file) applications. Given that the configuration assemblies have actually been loaded, using `AppDomain.CurrentDomain.GetAssemblies()` works for both bundled and non bundled applications.
1 parent b46a5f9 commit cad35a0

File tree

9 files changed

+257
-4
lines changed

9 files changed

+257
-4
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,20 @@ For legacy .NET Framework projects it also scans default probing path(s).
115115

116116
For all other cases, as well as in the case of non-conventional configuration assembly names **DO** use [Using](#using-section-and-auto-discovery-of-configuration-assemblies) section.
117117

118-
#### .NET 5.0 Single File Applications
118+
#### .NET Single File Applications
119119

120-
Currently, auto-discovery of configuration assemblies is not supported in bundled mode. **DO** use [Using](#using-section-and-auto-discovery-of-configuration-assemblies) section for workaround.
120+
Currently, auto-discovery of configuration assemblies requires special care when the application is deployed in [bundled mode](https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file/overview).
121+
122+
Starting with version 3.4.0, the configuration assemblies must be explicitly loaded prior to calling `loggerConfiguration.ReadFrom.Configuration(…)`.
123+
124+
```csharp
125+
// Force loading of assemblies that might be used in the configuration (required when published as single file)
126+
_ = typeof(ConsoleLoggerConfigurationExtensions).Assembly;
127+
_ = typeof(SeqLoggerConfigurationExtensions).Assembly;
128+
loggerConfiguration.ReadFrom.Configuration(context.Configuration);
129+
```
130+
131+
For versions older than 3.4.0, the [Using](#using-section-and-auto-discovery-of-configuration-assemblies) section must be used to load configuration assemblies as a workaround.
121132

122133
### MinimumLevel, LevelSwitches, overrides and dynamic reload
123134

serilog-settings-configuration.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sam
3131
EndProject
3232
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDummies\TestDummies.csproj", "{B7CF5068-DD19-4868-A268-5280BDE90361}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestSingleFileApp", "test\TestSingleFileApp\TestSingleFileApp.csproj", "{C959B095-5B29-4663-A5B6-4742D5EA97AB}"
35+
EndProject
3436
Global
3537
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3638
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +55,10 @@ Global
5355
{B7CF5068-DD19-4868-A268-5280BDE90361}.Debug|Any CPU.Build.0 = Debug|Any CPU
5456
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.ActiveCfg = Release|Any CPU
5557
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.Build.0 = Release|Any CPU
58+
{C959B095-5B29-4663-A5B6-4742D5EA97AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59+
{C959B095-5B29-4663-A5B6-4742D5EA97AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
60+
{C959B095-5B29-4663-A5B6-4742D5EA97AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{C959B095-5B29-4663-A5B6-4742D5EA97AB}.Release|Any CPU.Build.0 = Release|Any CPU
5662
EndGlobalSection
5763
GlobalSection(SolutionProperties) = preSolution
5864
HideSolutionNode = FALSE
@@ -62,6 +68,7 @@ Global
6268
{F793C6E8-C40A-4018-8884-C97E2BE38A54} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6369
{A00E5E32-54F9-401A-BBA1-2F6FCB6366CD} = {D24872B9-57F3-42A7-BC8D-F9DA222FCE1B}
6470
{B7CF5068-DD19-4868-A268-5280BDE90361} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
71+
{C959B095-5B29-4663-A5B6-4742D5EA97AB} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6572
EndGlobalSection
6673
GlobalSection(ExtensibilityGlobals) = postSolution
6774
SolutionGuid = {485F8843-42D7-4267-B5FB-20FE9181DEE9}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
6+
namespace Serilog.Settings.Configuration.Assemblies
7+
{
8+
class AppDomainAssemblyFinder : AssemblyFinder
9+
{
10+
public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
11+
{
12+
var query = from assembly in AppDomain.CurrentDomain.GetAssemblies()
13+
let assemblyName = assembly.GetName()
14+
where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind)
15+
select assemblyName;
16+
17+
return query.ToList();
18+
}
19+
}
20+
}

src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Reflection;
44
using Microsoft.Extensions.DependencyModel;
@@ -22,7 +22,7 @@ public static AssemblyFinder Auto()
2222
// `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null
2323
if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null)
2424
{
25-
return new DependencyContextAssemblyFinder(DependencyContext.Default);
25+
return new CompositeAssemblyFinder(new AppDomainAssemblyFinder(), new DependencyContextAssemblyFinder(DependencyContext.Default));
2626
}
2727
}
2828
catch (NotSupportedException) when (typeof(object).Assembly.Location is "") // bundled mode detection
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
6+
namespace Serilog.Settings.Configuration.Assemblies
7+
{
8+
class CompositeAssemblyFinder : AssemblyFinder
9+
{
10+
readonly AssemblyFinder[] _assemblyFinders;
11+
12+
public CompositeAssemblyFinder(params AssemblyFinder[] assemblyFinders)
13+
{
14+
if (assemblyFinders == null) throw new ArgumentNullException(nameof(assemblyFinders));
15+
if (assemblyFinders.Length == 0) throw new ArgumentException("The assembly finders must not be empty", nameof(assemblyFinders));
16+
_assemblyFinders = assemblyFinders;
17+
}
18+
19+
public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
20+
{
21+
var assemblyNames = new HashSet<AssemblyName>(SimpleNameComparer.Instance);
22+
foreach (var assemblyFinder in _assemblyFinders)
23+
{
24+
foreach (var assemblyName in assemblyFinder.FindAssembliesContainingName(nameToFind))
25+
{
26+
assemblyNames.Add(assemblyName);
27+
}
28+
}
29+
return assemblyNames.ToList();
30+
}
31+
32+
class SimpleNameComparer : IEqualityComparer<AssemblyName>
33+
{
34+
public static SimpleNameComparer Instance = new();
35+
36+
public bool Equals(AssemblyName x, AssemblyName y)
37+
{
38+
if (ReferenceEquals(x, y)) return true;
39+
if (ReferenceEquals(x, null)) return false;
40+
if (ReferenceEquals(y, null)) return false;
41+
if (x.GetType() != y.GetType()) return false;
42+
return x.Name == y.Name;
43+
}
44+
45+
public int GetHashCode(AssemblyName obj)
46+
{
47+
return obj.Name.GetHashCode();
48+
}
49+
}
50+
}
51+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#if !NETCOREAPP2_1
2+
using System.IO;
3+
using System.Runtime.CompilerServices;
4+
using System.Runtime.InteropServices;
5+
using Serilog.Settings.Configuration.Tests.Support;
6+
using Xunit;
7+
8+
namespace Serilog.Settings.Configuration.Tests
9+
{
10+
public class SingleFileAppTest
11+
{
12+
[Fact]
13+
void SingleFileApp()
14+
{
15+
var testDirectory = new DirectoryInfo(GetCurrentFilePath()).Parent?.Parent ?? throw new DirectoryNotFoundException("Can't find the 'test' directory");
16+
var workingDirectory = Path.Combine(testDirectory.FullName, "TestSingleFileApp");
17+
var publishDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
18+
19+
try
20+
{
21+
ProcessExtensions.RunDotnet(workingDirectory, "publish", "-c", "Release", "-o", publishDirectory);
22+
var exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestSingleFileApp.exe" : "TestSingleFileApp";
23+
var exePath = Path.Combine(publishDirectory, exeName);
24+
var result = ProcessExtensions.RunCommand(exePath);
25+
26+
Assert.Matches("^$", result.Error);
27+
Assert.Equal("Everything is working as expected", result.Output);
28+
}
29+
finally
30+
{
31+
try
32+
{
33+
Directory.Delete(publishDirectory, recursive: true);
34+
}
35+
catch
36+
{
37+
// Don't hide the actual exception, if any
38+
}
39+
}
40+
}
41+
42+
static string GetCurrentFilePath([CallerFilePath] string path = "") => path;
43+
}
44+
}
45+
#endif
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Diagnostics;
3+
4+
namespace Serilog.Settings.Configuration.Tests.Support
5+
{
6+
public static class ProcessExtensions
7+
{
8+
public struct CommandResult
9+
{
10+
public CommandResult(string output, string error)
11+
{
12+
Output = output;
13+
Error = error;
14+
}
15+
16+
public string Output { get; }
17+
public string Error { get; }
18+
}
19+
20+
public static void RunDotnet(string workingDirectory, params string[] args)
21+
{
22+
RunCommand("dotnet", useShellExecute: true, workingDirectory, args);
23+
}
24+
25+
public static CommandResult RunCommand(string command, params string[] args)
26+
{
27+
return RunCommand(command, useShellExecute: false, "", args);
28+
}
29+
30+
static CommandResult RunCommand(string command, bool useShellExecute, string workingDirectory, params string[] args)
31+
{
32+
var arguments = $"\"{string.Join("\" \"", args)}\"";
33+
var redirect = !useShellExecute;
34+
var startInfo = new ProcessStartInfo(command, arguments)
35+
{
36+
CreateNoWindow = true,
37+
UseShellExecute = useShellExecute,
38+
WorkingDirectory = workingDirectory,
39+
RedirectStandardOutput = redirect,
40+
RedirectStandardError = redirect,
41+
};
42+
var process = new Process { StartInfo = startInfo };
43+
process.Start();
44+
var timeout = TimeSpan.FromSeconds(30);
45+
var exited = process.WaitForExit((int)timeout.TotalMilliseconds);
46+
if (!exited)
47+
{
48+
throw new TimeoutException($"The command '{command} {arguments}' did not execute within {timeout.TotalSeconds} seconds");
49+
}
50+
51+
var error = redirect ? process.StandardError.ReadToEnd() : "";
52+
if (process.ExitCode != 0)
53+
{
54+
throw new InvalidOperationException($"The command '{command} {arguments}' exited with code {process.ExitCode}");
55+
}
56+
57+
var output = redirect ? process.StandardOutput.ReadToEnd() : "";
58+
return new CommandResult(output, error);
59+
}
60+
}
61+
}

test/TestSingleFileApp/Program.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.Extensions.Configuration;
4+
using Serilog;
5+
using Serilog.Debugging;
6+
7+
try
8+
{
9+
// Force loading of assemblies could become unnecessary if the [DependencyContextLoader][1]
10+
// starts supporting applications published as single-file in the future.
11+
// Unfortunately, as of .NET 6, loading the DependencyContext from a single-file application is not supported.
12+
// [1]: https://github.com/dotnet/runtime/blob/v6.0.3/src/libraries/Microsoft.Extensions.DependencyModel/src/DependencyContextLoader.cs#L54-L55
13+
_ = typeof(Serilog.Sinks.InMemory.InMemorySinkExtensions).Assembly;
14+
_ = typeof(ConsoleLoggerConfigurationExtensions).Assembly;
15+
16+
SelfLog.Enable(text => Console.Error.WriteLine(text));
17+
18+
var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>
19+
{
20+
["Serilog:WriteTo:0:Name"] = "InMemory",
21+
["Serilog:WriteTo:1:Name"] = "Console",
22+
["Serilog:WriteTo:1:Args:outputTemplate"] = "{Message:l}",
23+
}).Build();
24+
25+
using var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
26+
logger.Information("Everything is working as expected");
27+
}
28+
catch (Exception exception)
29+
{
30+
Console.Error.WriteLine($"An unexpected exception occurred: {exception}");
31+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net5.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<PropertyGroup>
10+
<DebugType>embedded</DebugType>
11+
<PublishSingleFile>true</PublishSingleFile>
12+
<SelfContained>false</SelfContained>
13+
<RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('Linux'))">linux-x64</RuntimeIdentifier>
14+
<RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('OSX'))">osx-x64</RuntimeIdentifier>
15+
<RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-x64</RuntimeIdentifier>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\..\src\Serilog.Settings.Configuration\Serilog.Settings.Configuration.csproj" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
24+
<PackageReference Include="Serilog.Sinks.InMemory" Version="0.6.0" />
25+
</ItemGroup>
26+
27+
</Project>

0 commit comments

Comments
 (0)