Skip to content

Commit 325577e

Browse files
authored
Merge pull request #353 from 0xced/single-app-no-assemblies-exception
Make sure that single-file apps can find assemblies that contains sinks
2 parents 246920e + e87d4e8 commit 325577e

18 files changed

+546
-24
lines changed

Build.ps1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ if($LASTEXITCODE -ne 0) { exit 2 }
3030

3131
Write-Output "build: Testing"
3232

33-
& dotnet test test\Serilog.Settings.Configuration.Tests --configuration Release --no-build --no-restore
33+
# Dotnet test doesn't run separate TargetFrameworks in parallel: https://github.com/dotnet/sdk/issues/19147
34+
# Workaround: use `dotnet test` on dlls directly in order to pass the `--parallel` option to vstest.
35+
# The _reported_ runtime is wrong but the _actual_ used runtime is correct, see https://github.com/microsoft/vstest/issues/2037#issuecomment-720549173
36+
& dotnet test test\Serilog.Settings.Configuration.Tests\bin\Release\*\Serilog.Settings.Configuration.Tests.dll --parallel
3437

3538
if($LASTEXITCODE -ne 0) { exit 3 }

serilog-settings-configuration.sln

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sam
3232
EndProject
3333
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDummies\TestDummies.csproj", "{B7CF5068-DD19-4868-A268-5280BDE90361}"
3434
EndProject
35+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}"
36+
EndProject
3537
Global
3638
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3739
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,8 @@ Global
5456
{B7CF5068-DD19-4868-A268-5280BDE90361}.Debug|Any CPU.Build.0 = Debug|Any CPU
5557
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.ActiveCfg = Release|Any CPU
5658
{B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Release|Any CPU.ActiveCfg = Release|Any CPU
5761
EndGlobalSection
5862
GlobalSection(SolutionProperties) = preSolution
5963
HideSolutionNode = FALSE
@@ -63,6 +67,7 @@ Global
6367
{F793C6E8-C40A-4018-8884-C97E2BE38A54} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6468
{A00E5E32-54F9-401A-BBA1-2F6FCB6366CD} = {D24872B9-57F3-42A7-BC8D-F9DA222FCE1B}
6569
{B7CF5068-DD19-4868-A268-5280BDE90361} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
70+
{1B6E08F3-16C9-4912-BEEE-57DB78C92A12} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}
6671
EndGlobalSection
6772
GlobalSection(ExtensibilityGlobals) = postSolution
6873
SolutionGuid = {485F8843-42D7-4267-B5FB-20FE9181DEE9}

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

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,7 @@ protected static bool IsCaseInsensitiveMatch(string? text, string textToFind)
1414

1515
public static AssemblyFinder Auto()
1616
{
17-
try
18-
{
19-
// Need to check `Assembly.GetEntryAssembly()` first because
20-
// `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null
21-
if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null)
22-
{
23-
return new DependencyContextAssemblyFinder(DependencyContext.Default);
24-
}
25-
}
26-
catch (NotSupportedException) when (typeof(object).Assembly.Location is "") // bundled mode detection
27-
{
28-
}
29-
30-
return new DllScanningAssemblyFinder();
17+
return new CompositeAssemblyFinder(new DependencyContextAssemblyFinder(DependencyContext.Default), new DllScanningAssemblyFinder());
3118
}
3219

3320
public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Reflection;
2+
3+
namespace Serilog.Settings.Configuration.Assemblies;
4+
5+
class CompositeAssemblyFinder : AssemblyFinder
6+
{
7+
readonly AssemblyFinder[] _assemblyFinders;
8+
9+
public CompositeAssemblyFinder(params AssemblyFinder[] assemblyFinders)
10+
{
11+
_assemblyFinders = assemblyFinders;
12+
}
13+
14+
public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
15+
{
16+
var assemblyNames = new List<AssemblyName>();
17+
foreach (var assemblyFinder in _assemblyFinders)
18+
{
19+
assemblyNames.AddRange(assemblyFinder.FindAssembliesContainingName(nameToFind));
20+
}
21+
return assemblyNames;
22+
}
23+
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ namespace Serilog.Settings.Configuration.Assemblies;
55

66
sealed class DependencyContextAssemblyFinder : AssemblyFinder
77
{
8-
readonly DependencyContext _dependencyContext;
8+
readonly DependencyContext? _dependencyContext;
99

10-
public DependencyContextAssemblyFinder(DependencyContext dependencyContext)
10+
public DependencyContextAssemblyFinder(DependencyContext? dependencyContext)
1111
{
12-
_dependencyContext = dependencyContext ?? throw new ArgumentNullException(nameof(dependencyContext));
12+
_dependencyContext = dependencyContext;
1313
}
1414

1515
public override IReadOnlyList<AssemblyName> FindAssembliesContainingName(string nameToFind)
1616
{
17+
if (_dependencyContext == null)
18+
return Array.Empty<AssemblyName>();
19+
1720
var query = from library in _dependencyContext.RuntimeLibraries
1821
where IsReferencingSerilog(library)
1922
from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext)
2023
where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind)
2124
select assemblyName;
2225

23-
return query.ToList().AsReadOnly();
26+
return query.ToList();
2427

2528
static bool IsReferencingSerilog(Library library)
2629
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ where IsCaseInsensitiveMatch(assemblyFileName, nameToFind)
4848
where assemblyName != null
4949
select assemblyName;
5050

51-
return query.ToList().AsReadOnly();
51+
return query.ToList();
5252

5353
static AssemblyName? TryGetAssemblyNameFrom(string path)
5454
{

src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfiguration
369369
{
370370
if (string.IsNullOrWhiteSpace(simpleName))
371371
throw new InvalidOperationException(
372-
"A zero-length or whitespace assembly name was supplied to a Serilog.Using configuration statement.");
372+
$"A zero-length or whitespace assembly name was supplied to a {usingSection.Path} configuration statement.");
373373

374374
var assembly = Assembly.Load(new AssemblyName(simpleName));
375375
if (!assemblies.ContainsKey(assembly.FullName!))
@@ -384,7 +384,19 @@ static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfiguration
384384
assemblies.Add(assumed.FullName!, assumed);
385385
}
386386

387-
return assemblies.Values.ToList().AsReadOnly();
387+
if (assemblies.Count == 1)
388+
{
389+
var message = $"""
390+
No {usingSection.Path} configuration section is defined and no Serilog assemblies were found.
391+
This is most likely because the application is published as single-file.
392+
Either add a {usingSection.Path} section or explicitly specify assemblies that contains sinks and other types through the reader options. For example:
393+
var options = new ConfigurationReaderOptions(typeof(ConsoleLoggerConfigurationExtensions).Assembly, typeof(SerilogExpression).Assembly);
394+
new LoggerConfiguration().ReadFrom.Configuration(configuration, options);
395+
""";
396+
throw new InvalidOperationException(message);
397+
}
398+
399+
return assemblies.Values;
388400
}
389401

390402
void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigurationArgumentValue>> methods, IReadOnlyCollection<MethodInfo> configurationMethods, object receiver)
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
using CliWrap;
4+
using CliWrap.Exceptions;
5+
using FluentAssertions;
6+
using FluentAssertions.Execution;
7+
using Serilog.Settings.Configuration.Tests.Support;
8+
using Xunit.Abstractions;
9+
10+
namespace Serilog.Settings.Configuration.Tests;
11+
12+
[Trait("Category", "Integration")]
13+
public sealed class PublishSingleFileTests : IDisposable, IClassFixture<TestApp>
14+
{
15+
readonly ITestOutputHelper _outputHelper;
16+
readonly TestApp _testApp;
17+
readonly AssertionScope _scope;
18+
19+
public PublishSingleFileTests(ITestOutputHelper outputHelper, TestApp testApp)
20+
{
21+
_outputHelper = outputHelper;
22+
_testApp = testApp;
23+
_scope = new AssertionScope();
24+
}
25+
26+
public void Dispose()
27+
{
28+
_scope.Dispose();
29+
}
30+
31+
[Theory]
32+
[ClassData(typeof(PublishModeTheoryData))]
33+
public async Task RunTestApp_NoUsingAndNoAssembly(PublishMode publishMode)
34+
{
35+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode);
36+
stdOut.Should().Be(isSingleFile ? "Expected exception" : "(Main thread) [Information] Expected success");
37+
stdErr.Should().BeEmpty();
38+
}
39+
40+
[Theory]
41+
[ClassData(typeof(PublishModeTheoryData))]
42+
public async Task RunTestApp_UsingConsole(PublishMode publishMode)
43+
{
44+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-console");
45+
stdOut.Should().Be(isSingleFile ? "() [Information] Expected success" : "(Main thread) [Information] Expected success");
46+
if (isSingleFile)
47+
stdErr.Should().Contain("Unable to find a method called WithThreadName");
48+
else
49+
stdErr.Should().BeEmpty();
50+
}
51+
52+
[Theory]
53+
[ClassData(typeof(PublishModeTheoryData))]
54+
public async Task RunTestApp_UsingThread(PublishMode publishMode)
55+
{
56+
var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-thread");
57+
stdOut.Should().Be(isSingleFile ? "" : "(Main thread) [Information] Expected success");
58+
if (isSingleFile)
59+
stdErr.Should().Contain("Unable to find a method called Console");
60+
else
61+
stdErr.Should().BeEmpty();
62+
}
63+
64+
[Theory]
65+
[ClassData(typeof(PublishModeTheoryData))]
66+
public async Task RunTestApp_AssemblyThread(PublishMode publishMode)
67+
{
68+
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-thread");
69+
stdOut.Should().BeEmpty();
70+
stdErr.Should().Contain("Unable to find a method called Console");
71+
}
72+
73+
[Theory]
74+
[ClassData(typeof(PublishModeTheoryData))]
75+
public async Task RunTestApp_AssemblyConsole(PublishMode publishMode)
76+
{
77+
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-console");
78+
stdOut.Should().Be("() [Information] Expected success");
79+
stdErr.Should().Contain("Unable to find a method called WithThreadName");
80+
}
81+
82+
[Theory]
83+
[ClassData(typeof(PublishModeAndStrategyTheoryData))]
84+
public async Task RunTestApp_ConsoleAndThread(PublishMode publishMode, string strategy)
85+
{
86+
var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, $"--{strategy}-console", $"--{strategy}-thread");
87+
stdOut.Should().Be("(Main thread) [Information] Expected success");
88+
stdErr.Should().BeEmpty();
89+
}
90+
91+
async Task<(bool IsSingleFile, string StdOut, string StdErr)> RunTestAppAsync(PublishMode publishMode, params string[] args)
92+
{
93+
// Determine whether the app is a _true_ single file, i.e. not a .NET Core 3.x version which
94+
// [extracts bundled files to disk][1] and thus can find dlls.
95+
// [1]: https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/extract.md
96+
var (isSingleFile, _) = await RunTestAppInternalAsync(publishMode, "is-single-file");
97+
var (stdOut, stdErr) = await RunTestAppInternalAsync(publishMode, args);
98+
return (bool.Parse(isSingleFile), stdOut, stdErr);
99+
}
100+
101+
async Task<(string StdOut, string StdErr)> RunTestAppInternalAsync(PublishMode publishMode, params string[] args)
102+
{
103+
var stdOutBuilder = new StringBuilder();
104+
var stdErrBuilder = new StringBuilder();
105+
106+
var command = Cli.Wrap(_testApp.GetExecutablePath(publishMode))
107+
.WithArguments(args)
108+
.WithValidation(CommandResultValidation.None)
109+
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuilder))
110+
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuilder));
111+
112+
_outputHelper.WriteLine(command.ToString());
113+
114+
var stopwatch = Stopwatch.StartNew();
115+
var result = await command.ExecuteAsync();
116+
var executionTime = stopwatch.ElapsedMilliseconds;
117+
118+
var stdOut = stdOutBuilder.ToString().Trim();
119+
var stdErr = stdErrBuilder.ToString().Trim();
120+
121+
_outputHelper.WriteLine($"Executed in {executionTime} ms");
122+
_outputHelper.WriteLine(stdOut.Length > 0 ? $"stdout: {stdOut}" : "nothing on stdout");
123+
_outputHelper.WriteLine(stdErr.Length > 0 ? $"stderr: {stdErr}" : "nothing on stderr");
124+
_outputHelper.WriteLine("");
125+
126+
if (result.ExitCode != 0)
127+
{
128+
throw new CommandExecutionException(command, result.ExitCode, $"An unexpected exception has occurred while running {command}{Environment.NewLine}{stdErr}".Trim());
129+
}
130+
131+
return (stdOut, stdErr);
132+
}
133+
134+
class PublishModeTheoryData : TheoryData<PublishMode>
135+
{
136+
public PublishModeTheoryData()
137+
{
138+
foreach (var publishMode in PublishModeExtensions.GetPublishModes())
139+
{
140+
Add(publishMode);
141+
}
142+
}
143+
}
144+
145+
class PublishModeAndStrategyTheoryData : TheoryData<PublishMode, string>
146+
{
147+
public PublishModeAndStrategyTheoryData()
148+
{
149+
foreach (var publishMode in PublishModeExtensions.GetPublishModes())
150+
{
151+
foreach (var strategy in new[] { "using", "assembly" })
152+
{
153+
Add(publishMode, strategy);
154+
}
155+
}
156+
}
157+
}
158+
}

test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT'">net48</TargetFrameworks>
5-
<TargetFrameworks>$(TargetFrameworks);net7.0</TargetFrameworks>
5+
<TargetFrameworks>$(TargetFrameworks);net7.0;net6.0</TargetFrameworks>
66
</PropertyGroup>
77

88
<ItemGroup>
@@ -17,8 +17,12 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20+
<PackageReference Include="CliWrap" Version="3.6.0" />
21+
<PackageReference Include="FluentAssertions" Version="6.10.0" />
2022
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
2123
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
24+
<PackageReference Include="NuGet.Frameworks" Version="6.5.0" />
25+
<PackageReference Include="Polly" Version="7.2.3" />
2226
<PackageReference Include="Serilog.Expressions" Version="3.3.0" />
2327
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
2428
<PackageReference Include="xunit" Version="2.4.2" />
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Serilog.Settings.Configuration.Tests;
2+
3+
static class DirectoryInfoExtensions
4+
{
5+
public static DirectoryInfo SubDirectory(this DirectoryInfo directory, params string[] paths)
6+
=> new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray())));
7+
8+
public static FileInfo File(this DirectoryInfo directory, params string[] paths)
9+
=> new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray())));
10+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Serilog.Settings.Configuration.Tests.Support;
2+
3+
/// <summary>
4+
/// The possible application publish modes for the TestApp.
5+
/// See also the <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/">.NET application publishing overview</a> documentation.
6+
/// </summary>
7+
public enum PublishMode
8+
{
9+
/// <summary>
10+
/// Standard app publish, all dlls and related files are copied along the main executable.
11+
/// </summary>
12+
Standard,
13+
14+
/// <summary>
15+
/// Publish a single file as a framework-dependent binary.
16+
/// </summary>
17+
/// <remarks>On .NET Framework, <a href="https://github.com/Fody/Costura">Costura</a> is used to publish as a single file.</remarks>
18+
SingleFile,
19+
20+
/// <summary>
21+
/// Publish a single file as a self contained binary, i.e. including the .NET libraries and target runtime.
22+
/// </summary>
23+
/// <remarks>This mode is ignored on .NET Framework as it doesn't make sense.</remarks>
24+
SelfContained,
25+
}

0 commit comments

Comments
 (0)