Skip to content
This repository was archived by the owner on Jan 24, 2021. It is now read-only.

Commit 9e3b038

Browse files
Merge pull request #2654 from arturosevilla/assembly-catalog-unloads-after-detection
AppDomainAssemblyCatalog correctly unloads the assemblies that were probed for any Nancy reference.
2 parents d60c086 + 4edb010 commit 9e3b038

File tree

6 files changed

+150
-19
lines changed

6 files changed

+150
-19
lines changed

src/Nancy.MSBuild/Nancy.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@
357357
<Compile Include="..\Nancy\Helpers\ExceptionExtensions.cs">
358358
<Link>Helpers\ExceptionExtensions.cs</Link>
359359
</Compile>
360+
<Compile Include="..\Nancy\Helpers\ProxyNancyReferenceProber.cs">
361+
<Link>Helpers\ProxyNancyReferenceProber.cs</Link>
362+
</Compile>
360363
<Compile Include="..\Nancy\HttpLink.cs">
361364
<Link>HttpLink.cs</Link>
362365
</Compile>

src/Nancy/AppDomainAssemblyCatalog.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ namespace Nancy
66
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9+
using Nancy.Extensions;
10+
using Nancy.Helpers;
911

1012
/// <summary>
1113
/// Default implementation of the <see cref="IAssemblyCatalog"/> interface, based on
@@ -40,7 +42,7 @@ private static List<Assembly> GetLoadedNancyReferencingAssemblies()
4042

4143
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
4244
{
43-
if (!assembly.IsDynamic && !assembly.ReflectionOnly && IsNancyReferencing(assembly))
45+
if (!assembly.IsDynamic && !assembly.ReflectionOnly && assembly.IsReferencing(NancyAssemblyName))
4446
{
4547
assemblies.Add(assembly);
4648
}
@@ -52,7 +54,8 @@ private static List<Assembly> GetLoadedNancyReferencingAssemblies()
5254
private static IEnumerable<Assembly> LoadNancyReferencingAssemblies(IEnumerable<Assembly> loadedAssemblies)
5355
{
5456
var assemblies = new HashSet<Assembly>();
55-
var inspectionAppDomain = AppDomain.CreateDomain("AppDomainAssemblyCatalog");
57+
var inspectionAppDomain = CreateInspectionAppDomain();
58+
var inspectionProber = CreateRemoteReferenceProber(inspectionAppDomain);
5659
var loadedNancyReferencingAssemblyNames = loadedAssemblies.Select(assembly => assembly.GetName()).ToArray();
5760

5861
foreach (var directory in GetAssemblyDirectories())
@@ -68,9 +71,7 @@ private static IEnumerable<Assembly> LoadNancyReferencingAssemblies(IEnumerable<
6871

6972
if (!loadedNancyReferencingAssemblyNames.Any(loadedNancyReferencingAssemblyName => AssemblyName.ReferenceMatchesDefinition(loadedNancyReferencingAssemblyName, unloadedAssemblyName)))
7073
{
71-
var inspectionAssembly = SafeLoadAssembly(inspectionAppDomain, unloadedAssemblyName);
72-
73-
if (inspectionAssembly != null && IsNancyReferencing(inspectionAssembly))
74+
if (inspectionProber.HasReference(unloadedAssemblyName, NancyAssemblyName))
7475
{
7576
var assembly = SafeLoadAssembly(AppDomain.CurrentDomain, unloadedAssemblyName);
7677

@@ -88,22 +89,19 @@ private static IEnumerable<Assembly> LoadNancyReferencingAssemblies(IEnumerable<
8889
return assemblies.ToArray();
8990
}
9091

91-
private static bool IsNancyReferencing(Assembly assembly)
92+
private static AppDomain CreateInspectionAppDomain()
9293
{
93-
if (AssemblyName.ReferenceMatchesDefinition(assembly.GetName(), NancyAssemblyName))
94-
{
95-
return true;
96-
}
94+
var currentAppDomain = AppDomain.CurrentDomain;
9795

98-
foreach (var referencedAssemblyName in assembly.GetReferencedAssemblies())
99-
{
100-
if (AssemblyName.ReferenceMatchesDefinition(referencedAssemblyName, NancyAssemblyName))
101-
{
102-
return true;
103-
}
104-
}
96+
return AppDomain.CreateDomain("AppDomainAssemblyCatalog", currentAppDomain.Evidence,
97+
currentAppDomain.SetupInformation);
98+
}
10599

106-
return false;
100+
private static ProxyNancyReferenceProber CreateRemoteReferenceProber(AppDomain appDomain)
101+
{
102+
return (ProxyNancyReferenceProber)appDomain.CreateInstanceAndUnwrap(
103+
typeof(ProxyNancyReferenceProber).Assembly.FullName,
104+
typeof(ProxyNancyReferenceProber).FullName);
107105
}
108106

109107
private static IEnumerable<string> GetAssemblyDirectories()

src/Nancy/Extensions/AssemblyExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,31 @@ public static Type[] SafeGetExportedTypes(this Assembly assembly)
3737
}
3838
return types;
3939
}
40+
41+
#if !CORE
42+
/// <summary>
43+
/// Indicates if a given assembly references another which is identified by its name.
44+
/// </summary>
45+
/// <param name="assembly">The assembly which will be probed.</param>
46+
/// <param name="referenceName">The reference assembly name.</param>
47+
/// <returns>A boolean value indicating if there is a reference.</returns>
48+
public static bool IsReferencing(this Assembly assembly, AssemblyName referenceName)
49+
{
50+
if (AssemblyName.ReferenceMatchesDefinition(assembly.GetName(), referenceName))
51+
{
52+
return true;
53+
}
54+
55+
foreach (var referencedAssemblyName in assembly.GetReferencedAssemblies())
56+
{
57+
if (AssemblyName.ReferenceMatchesDefinition(referencedAssemblyName, referenceName))
58+
{
59+
return true;
60+
}
61+
}
62+
63+
return false;
64+
}
65+
#endif
4066
}
4167
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#if !CORE
2+
namespace Nancy.Helpers
3+
{
4+
using System;
5+
using System.Reflection;
6+
using Nancy.Extensions;
7+
8+
/// <summary>
9+
/// Utility class used to probe assembly references.
10+
/// </summary>
11+
/// <remarks>
12+
/// Because this class inherits from <see cref="T:System.MarshalByRefObject"/> it can be used across different <see cref="T:System.AppDomain"/>.
13+
/// </remarks>
14+
internal class ProxyNancyReferenceProber : MarshalByRefObject
15+
{
16+
/// <summary>
17+
/// Determines if the assembly has a reference (dependency) upon another one.
18+
/// </summary>
19+
/// <param name="assemblyNameForProbing">The name of the assembly that will be tested.</param>
20+
/// <param name="referenceAssemblyName">The reference assembly name.</param>
21+
/// <returns>A boolean value indicating if there is a reference.</returns>
22+
public bool HasReference(AssemblyName assemblyNameForProbing, AssemblyName referenceAssemblyName)
23+
{
24+
var assemblyForInspection = Assembly.ReflectionOnlyLoad(assemblyNameForProbing.Name);
25+
26+
return assemblyForInspection.IsReferencing(referenceAssemblyName);
27+
}
28+
}
29+
}
30+
#endif

test/Nancy.Tests.MSBuild/Nancy.Tests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,9 @@
649649
<Compile Include="..\Nancy.Tests\Unit\Responses\GenericFileResponseFixture.cs">
650650
<Link>Unit\Responses\GenericFileResponseFixture.cs</Link>
651651
</Compile>
652+
<Compile Include="..\Nancy.Tests\Unit\AppDomainAssemblyCatalogFixture.cs">
653+
<Link>Unit\AppDomainAssemblyCatalogFixture.cs</Link>
654+
</Compile>
652655
</ItemGroup>
653656
<ItemGroup>
654657
<ProjectReference Include="..\..\src\Nancy.Testing.MSBuild\Nancy.Testing.csproj">
@@ -744,4 +747,4 @@
744747
<Target Name="AfterBuild">
745748
</Target>
746749
-->
747-
</Project>
750+
</Project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace Nancy.Tests.Unit
2+
{
3+
using System;
4+
using System.CodeDom.Compiler;
5+
using System.Linq;
6+
using System.Reflection;
7+
using Xunit;
8+
9+
public class AppDomainAssemblyCatalogFixture
10+
{
11+
[Fact]
12+
public void Modules_without_Nancy_references_should_not_keep_loaded_after_inspection()
13+
{
14+
// Given
15+
var compilerAppDomain = AppDomain.CreateDomain("AssemblyGenerator",
16+
AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation);
17+
18+
var assemblyGenerator = (ProxyAssemblyGenerator)compilerAppDomain.CreateInstanceAndUnwrap(
19+
typeof(ProxyAssemblyGenerator).Assembly.FullName,
20+
typeof(ProxyAssemblyGenerator).FullName);
21+
22+
try
23+
{
24+
var generatedAssemblyName = assemblyGenerator.GenerateAssemblyAndGetName();
25+
26+
var assemblyCatalog = new AppDomainAssemblyCatalog();
27+
28+
// When
29+
30+
// the following call will load the assemblies into its own inspection AppDomain
31+
// and release the assemblies that do not reference Nancy afterwards,
32+
// keeping the application AppDomain free of such assemblies, that is, the created
33+
// assembly should not be loaded by AppDomain.GetAssemblies after this call
34+
assemblyCatalog.GetAssemblies();
35+
36+
var loadedAssembliesAfterInspection = AppDomain.CurrentDomain.GetAssemblies();
37+
38+
// Then
39+
loadedAssembliesAfterInspection
40+
.Select(assembly => assembly.GetName().Name)
41+
.Contains(generatedAssemblyName.Name)
42+
.ShouldBeFalse();
43+
}
44+
finally
45+
{
46+
AppDomain.Unload(compilerAppDomain);
47+
}
48+
}
49+
50+
private class ProxyAssemblyGenerator : MarshalByRefObject
51+
{
52+
public AssemblyName GenerateAssemblyAndGetName()
53+
{
54+
var generatedAssembly = CodeDomProvider
55+
.CreateProvider("CSharp")
56+
.CompileAssemblyFromSource(
57+
new CompilerParameters
58+
{
59+
GenerateInMemory = true,
60+
GenerateExecutable = false,
61+
IncludeDebugInformation = false,
62+
OutputAssembly = "AssemblyShouldNotBeLoadedIntoAppDomain.dll"
63+
},
64+
"public class DummyClass { }")
65+
.CompiledAssembly;
66+
67+
return generatedAssembly.GetName();
68+
}
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)