DOTNET_STARTUP_HOOKS=D:\path\to\StartupHook1.dll;D:\path\to\StartupHook2.dll
```
-This variable is a list of absolute assembly paths, delimited by the
+This variable is a list of assembly paths or names, delimited by the
platform-specific path separator (`;` on Windows and `:` on Unix). It
-may not contain any empty entries or a trailing path separator. The
+may contain leading, trailing or duplicate path separators. The
type must be named `StartupHook` without any namespace, and should be
`internal`.
+Each part may be either
+* absolute path to the assembly with the startup hook. In this case
+ the assembly is loaded from the specified path before running
+ the startup hook.
+* name of the assembly with the startup hook. In this case the assembly
+ is loaded by its name from the `AssemblyLoadContext.Default`. For
+ this to work the assembly needs to be part of the application
+ otherwise the default context won't be able to resolve it. The assembly
+ name must not be a relative path, so the following rules apply
+ * the assembly name must not contain directory separator characters
+ `/` and `\`
+ * the assembly name must not contain the space characters ` ` and
+ the comma character `,`
+ * the assembly name must not end with `.dll` (any casing)
+ * the assembly name must be considered a valid assembly name as specified
+ by the `AssemblyName` class.
+
+Note that white-spaces are preserved and considered part of the specified
+path/name. So for example path separator followed by a white-space and
+another path separator is invalid, since the white-space only string
+in between the path separators will be considered as assembly name.
+
Setting this environment variable will cause the `public static void
Initialize()` method of the `StartupHook` type in each of the
specified assemblies to be called in order, synchronously, before the
.And.HaveStdErrContaining("System.IO.FileNotFoundException: Could not load file or assembly 'Newtonsoft.Json");
}
- // Run the app with an invalid syntax in startup hook variable
+ // Different variants of the startup hook variable format
[Fact]
- public void Muxer_activation_of_Invalid_StartupHook_Fails()
+ public void Muxer_activation_of_StartupHook_VariableVariants()
{
var fixture = sharedTestState.PortableAppFixture.Copy();
var dotnet = fixture.BuiltDotnet;
var startupHookFixture = sharedTestState.StartupHookFixture.Copy();
var startupHookDll = startupHookFixture.TestProject.AppDll;
- var fakeAssembly = Path.GetFullPath("Assembly.dll");
- var fakeAssembly2 = Path.GetFullPath("Assembly2.dll");
-
- var expectedError = "System.ArgumentException: The syntax of the startup hook variable was invalid.";
+ var startupHook2Fixture = sharedTestState.StartupHookWithDependencyFixture.Copy();
+ var startupHook2Dll = startupHook2Fixture.TestProject.AppDll;
// Missing entries in the hook
- var startupHookVar = fakeAssembly + Path.PathSeparator + Path.PathSeparator + fakeAssembly2;
+ var startupHookVar = startupHookDll + Path.PathSeparator + Path.PathSeparator + startupHook2Dll;
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello from startup hook!")
+ .And.HaveStdOutContaining("Hello from startup hook with dependency!")
+ .And.HaveStdOutContaining("Hello World");
+
+ // Whitespace is invalid
+ startupHookVar = startupHookDll + Path.PathSeparator + " " + Path.PathSeparator + startupHook2Dll;
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookVar)
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
.Should().Fail()
- .And.HaveStdErrContaining(expectedError);
+ .And.HaveStdErrContaining("System.ArgumentException: The startup hook simple assembly name ' ' is invalid.");
// Leading separator
startupHookVar = Path.PathSeparator + startupHookDll;
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
- .Should().Fail()
- .And.HaveStdErrContaining(expectedError);
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello from startup hook!")
+ .And.HaveStdOutContaining("Hello World");
// Trailing separator
- startupHookVar = fakeAssembly + Path.PathSeparator + fakeAssembly2 + Path.PathSeparator;
+ startupHookVar = startupHookDll + Path.PathSeparator + startupHook2Dll + Path.PathSeparator;
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello from startup hook!")
+ .And.HaveStdOutContaining("Hello from startup hook with dependency!")
+ .And.HaveStdOutContaining("Hello World");
+ }
+
+ [Fact]
+ public void Muxer_activation_of_StartupHook_With_Invalid_Simple_Name_Fails()
+ {
+ var fixture = sharedTestState.PortableAppFixture.Copy();
+ var dotnet = fixture.BuiltDotnet;
+ var appDll = fixture.TestProject.AppDll;
+
+ var startupHookFixture = sharedTestState.StartupHookFixture.Copy();
+ var startupHookDll = startupHookFixture.TestProject.AppDll;
+
+ var relativeAssemblyPath = $".{Path.DirectorySeparatorChar}Assembly";
+
+ var expectedError = "System.ArgumentException: The startup hook simple assembly name '{0}' is invalid.";
+
+ // With directory separator
+ var startupHookVar = relativeAssemblyPath;
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookVar)
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
.Should().Fail()
- .And.HaveStdErrContaining(expectedError);
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.NotHaveStdErrContaining("--->");
+
+ // With alternative directory separator
+ startupHookVar = $".{Path.AltDirectorySeparatorChar}Assembly";
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.NotHaveStdErrContaining("--->");
- // Syntax errors are caught before any hooks run
- startupHookVar = startupHookDll + Path.PathSeparator;
+ // With comma
+ startupHookVar = $"Assembly,version=1.0.0.0";
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookVar)
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
.Should().Fail()
- .And.HaveStdErrContaining(expectedError)
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.NotHaveStdErrContaining("--->");
+
+ // With space
+ startupHookVar = $"Assembly version";
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.NotHaveStdErrContaining("--->");
+
+ // With .dll suffix
+ startupHookVar = $".{Path.AltDirectorySeparatorChar}Assembly.DLl";
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.NotHaveStdErrContaining("--->");
+
+ // With invalid name
+ startupHookVar = $"Assembly=Name";
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.HaveStdErrContaining("---> System.IO.FileLoadException: The given assembly name or codebase was invalid.");
+
+ // Relative path error is caught before any hooks run
+ startupHookVar = startupHookDll + Path.PathSeparator + relativeAssemblyPath;
+ dotnet.Exec(appDll)
+ .EnvironmentVariable(startupHookVarName, startupHookVar)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute(fExpectedToFail: true)
+ .Should().Fail()
+ .And.HaveStdErrContaining(string.Format(expectedError, relativeAssemblyPath))
.And.NotHaveStdOutContaining("Hello from startup hook!");
}
- // Run the app with a relative path to the startup hook assembly
[Fact]
- public void Muxer_activation_of_StartupHook_With_Relative_Path_Fails()
+ public void Muxer_activation_of_StartupHook_With_Missing_Assembly_Fails()
{
var fixture = sharedTestState.PortableAppFixture.Copy();
var dotnet = fixture.BuiltDotnet;
var startupHookFixture = sharedTestState.StartupHookFixture.Copy();
var startupHookDll = startupHookFixture.TestProject.AppDll;
- var relativeAssemblyPath = "Assembly.dll";
+ var expectedError = "System.ArgumentException: Startup hook assembly '{0}' failed to load.";
- var expectedError = "System.ArgumentException: Absolute path information is required.";
-
- // Relative path
- var startupHookVar = relativeAssemblyPath;
+ // With file path which doesn't exist
+ var startupHookVar = startupHookDll + ".missing.dll";
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookVar)
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
.Should().Fail()
- .And.HaveStdErrContaining(expectedError);
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.HaveStdErrContaining($"---> System.IO.FileNotFoundException: Could not load file or assembly '{startupHookVar}'. The system cannot find the file specified.");
- // Relative path error is caught before any hooks run
- startupHookVar = startupHookDll + Path.PathSeparator + "Assembly.dll";
+ // With simple name which won't resolve
+ startupHookVar = "MissingAssembly";
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookVar)
.CaptureStdOut()
.CaptureStdErr()
.Execute(fExpectedToFail: true)
.Should().Fail()
- .And.HaveStdErrContaining(expectedError)
- .And.NotHaveStdOutContaining("Hello from startup hook!");
+ .And.HaveStdErrContaining(string.Format(expectedError, startupHookVar))
+ .And.HaveStdErrContaining($"---> System.IO.FileNotFoundException: Could not load file or assembly '{startupHookVar}");
+ }
+
+ [Fact]
+ public void Muxer_activation_of_StartupHook_WithSimpleAssemblyName_Succeeds()
+ {
+ var fixture = sharedTestState.PortableAppFixture.Copy();
+ var startupHookFixture = sharedTestState.StartupHookFixture.Copy();
+ var startupHookDll = startupHookFixture.TestProject.AppDll;
+ var startupHookAssemblyName = Path.GetFileNameWithoutExtension(startupHookDll);
+
+ File.Copy(startupHookDll, Path.Combine(fixture.TestProject.BuiltApp.Location, Path.GetFileName(startupHookDll)));
+
+ SharedFramework.AddReferenceToDepsJson(
+ fixture.TestProject.DepsJson,
+ $"{fixture.TestProject.AssemblyName}/1.0.0",
+ startupHookAssemblyName,
+ "1.0.0");
+
+ fixture.BuiltDotnet.Exec(fixture.TestProject.AppDll)
+ .EnvironmentVariable(startupHookVarName, startupHookAssemblyName)
+ .CaptureStdOut()
+ .CaptureStdErr()
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOutContaining("Hello from startup hook!")
+ .And.HaveStdOutContaining("Hello World");
}
// Run the app with missing startup hook assembly
// Run the app with a startup hook that doesn't have any Initialize method
[Fact]
- public void Muxer_activation_of_StartupHook_With_Missing_Method()
+ public void Muxer_activation_of_StartupHook_With_Missing_Method_Fails()
{
var fixture = sharedTestState.PortableAppFixture.Copy();
var dotnet = fixture.BuiltDotnet;