diff --git a/StreamJsonRpc.slnx b/StreamJsonRpc.slnx
index f7ef07249..9cce2c878 100644
--- a/StreamJsonRpc.slnx
+++ b/StreamJsonRpc.slnx
@@ -32,5 +32,6 @@
+
diff --git a/docfx/docs/proxies.md b/docfx/docs/proxies.md
index d0116335c..704656ea4 100644
--- a/docfx/docs/proxies.md
+++ b/docfx/docs/proxies.md
@@ -61,3 +61,32 @@ In which case, you can declare your server methods to also return @System.Thread
Sometimes a client may need to block its caller until a response to a JSON-RPC request comes back.
The proxy maintains the same async-only contract that is exposed by the class itself.
[Learn more about sending requests](sendrequest.md), particularly under the heading about async responses.
+
+## Dynamic proxies
+
+The following concerns are related specifically to dynamically generated proxies and do not apply to source generated proxies.
+
+### AssemblyLoadContext considerations
+
+When in a .NET process with multiple (ALC) instances, you should consider whether StreamJsonRpc is loaded in an ALC that can load all the types required by the proxy interface.
+
+By default, StreamJsonRpc will generate dynamic proxies in the ALC that the (first) interface requested for the proxy is loaded within.
+This is usually the right choice because the interface should be in an ALC that can resolve all the interface's type references.
+When you request a proxy that implements *multiple* interfaces, and if those interfaces are loaded in different ALCs, you *may* need to control which ALC the proxy is generated in.
+The need to control this may manifest as an or due to types loading into multiple ALC instances.
+
+In such cases, you may control the ALC used to generate the proxy by surrounding your proxy request with a call to (and disposal of its result).
+
+For example, you might use the following code when StreamJsonRpc is loaded into a different ALC from your own code:
+
+```cs
+// Whatever ALC can resolve *all* type references in *all* proxy interfaces.
+AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(MethodBase.GetCurrentMethod()!.DeclaringType!.Assembly);
+IFoo proxy;
+using (AssemblyLoadContext.EnterContextualReflection(alc))
+{
+ proxy = (IFoo)jsonRpc.Attach([typeof(IFoo), typeof(IFoo2)]);
+}
+```
+
+This initializes the `proxy` local variable with a proxy that will be able to load all types that your own can load.
diff --git a/src/StreamJsonRpc/AssemblyNameEqualityComparer.cs b/src/StreamJsonRpc/AssemblyNameEqualityComparer.cs
new file mode 100644
index 000000000..253a15164
--- /dev/null
+++ b/src/StreamJsonRpc/AssemblyNameEqualityComparer.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+
+using System.Reflection;
+
+namespace StreamJsonRpc;
+
+internal class AssemblyNameEqualityComparer : IEqualityComparer
+{
+ internal static readonly IEqualityComparer Instance = new AssemblyNameEqualityComparer();
+
+ private AssemblyNameEqualityComparer()
+ {
+ }
+
+ public bool Equals(AssemblyName? x, AssemblyName? y)
+ {
+ if (x is null && y is null)
+ {
+ return true;
+ }
+
+ if (x is null || y is null)
+ {
+ return false;
+ }
+
+ return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public int GetHashCode(AssemblyName obj)
+ {
+ Requires.NotNull(obj, nameof(obj));
+
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullName);
+ }
+}
diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs
index 7b4cae738..f0025c975 100644
--- a/src/StreamJsonRpc/ProxyGeneration.cs
+++ b/src/StreamJsonRpc/ProxyGeneration.cs
@@ -6,6 +6,9 @@
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
+#if NET
+using System.Runtime.Loader;
+#endif
using Microsoft.VisualStudio.Threading;
using CodeGenHelpers = StreamJsonRpc.Reflection.CodeGenHelpers;
@@ -19,7 +22,11 @@ namespace StreamJsonRpc;
[RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)]
internal static class ProxyGeneration
{
+#if NET
+ private static readonly List<(AssemblyLoadContext, ImmutableHashSet SkipVisibilitySet, ModuleBuilder Builder)> TransparentProxyModuleBuilderByVisibilityCheck = [];
+#else
private static readonly List<(ImmutableHashSet SkipVisibilitySet, ModuleBuilder Builder)> TransparentProxyModuleBuilderByVisibilityCheck = new List<(ImmutableHashSet, ModuleBuilder)>();
+#endif
private static readonly object BuilderLock = new object();
private static readonly AssemblyName ProxyAssemblyName = new AssemblyName(string.Format(CultureInfo.InvariantCulture, "StreamJsonRpc_Proxies_{0}", Guid.NewGuid()));
private static readonly MethodInfo DelegateCombineMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Combine), new Type[] { typeof(Delegate), typeof(Delegate) })!;
@@ -110,6 +117,9 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan addition
// Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations.
SortRpcInterfaces(rpcInterfaces);
+ // For ALC selection reasons, it's vital that the *user's* selected interfaces come *before* our own supporting interfaces.
+ // If the order is incorrect, type resolution may fail or the wrong AssemblyLoadContext (ALC) may be selected,
+ // leading to runtime errors or unexpected behavior when loading types or invoking methods.
Type[] proxyInterfaces = [.. rpcInterfaces.Select(i => i.Type), typeof(IJsonRpcClientProxy), typeof(IJsonRpcClientProxyInternal)];
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(proxyInterfaces);
@@ -783,10 +793,27 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
// For each set of skip visibility check assemblies, we need a dynamic assembly that skips at *least* that set.
// The CLR will not honor any additions to that set once the first generated type is closed.
// We maintain a dictionary to point at dynamic modules based on the set of skip visibility check assemblies they were generated with.
- ImmutableHashSet skipVisibilityCheckAssemblies = ImmutableHashSet.CreateRange(interfaceTypes.SelectMany(t => SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(t.GetTypeInfo())))
+ ImmutableHashSet skipVisibilityCheckAssemblies = ImmutableHashSet.CreateRange(AssemblyNameEqualityComparer.Instance, interfaceTypes.SelectMany(t => SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(t.GetTypeInfo())))
.Add(typeof(ProxyGeneration).Assembly.GetName());
+#if NET
+ // We have to key the dynamic assembly by ALC as well, since callers may set a custom contextual reflection context
+ // that influences how the assembly will resolve its type references.
+ // If they haven't set a contextual one, we assume the ALC that defines the (first) proxy interface.
+ AssemblyLoadContext alc = AssemblyLoadContext.CurrentContextualReflectionContext
+ ?? AssemblyLoadContext.GetLoadContext(interfaceTypes[0].Assembly)
+ ?? AssemblyLoadContext.GetLoadContext(typeof(ProxyGeneration).Assembly)
+ ?? throw new Exception("No ALC for our own assembly!");
+ foreach ((AssemblyLoadContext AssemblyLoadContext, ImmutableHashSet SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
+ {
+ if (existingSet.AssemblyLoadContext != alc)
+ {
+ continue;
+ }
+
+#else
foreach ((ImmutableHashSet SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
{
+#endif
if (existingSet.SkipVisibilitySet.IsSupersetOf(skipVisibilityCheckAssemblies))
{
return existingSet.Builder;
@@ -798,11 +825,22 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
// I have disabled this optimization though till we need it since it would sometimes cover up any bugs in the above visibility checking code.
////skipVisibilityCheckAssemblies = skipVisibilityCheckAssemblies.Union(AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName()));
- AssemblyBuilder assemblyBuilder = CreateProxyAssemblyBuilder();
+ AssemblyBuilder assemblyBuilder;
+#if NET
+ using (alc.EnterContextualReflection())
+#endif
+ {
+ assemblyBuilder = CreateProxyAssemblyBuilder();
+ }
+
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("rpcProxies");
var skipClrVisibilityChecks = new SkipClrVisibilityChecks(assemblyBuilder, moduleBuilder);
skipClrVisibilityChecks.SkipVisibilityChecksFor(skipVisibilityCheckAssemblies);
+#if NET
+ TransparentProxyModuleBuilderByVisibilityCheck.Add((alc, skipVisibilityCheckAssemblies, moduleBuilder));
+#else
TransparentProxyModuleBuilderByVisibilityCheck.Add((skipVisibilityCheckAssemblies, moduleBuilder));
+#endif
return moduleBuilder;
}
diff --git a/src/StreamJsonRpc/SkipClrVisibilityChecks.cs b/src/StreamJsonRpc/SkipClrVisibilityChecks.cs
index 6f094d9ab..491d11b20 100644
--- a/src/StreamJsonRpc/SkipClrVisibilityChecks.cs
+++ b/src/StreamJsonRpc/SkipClrVisibilityChecks.cs
@@ -82,7 +82,7 @@ internal static ImmutableHashSet GetSkipVisibilityChecksRequiremen
Requires.NotNull(typeInfo, nameof(typeInfo));
var visitedTypes = new HashSet();
- ImmutableHashSet.Builder assembliesDeclaringInternalTypes = ImmutableHashSet.CreateBuilder(AssemblyNameEqualityComparer.Instance);
+ ImmutableHashSet.Builder assembliesDeclaringInternalTypes = ImmutableHashSet.CreateBuilder(AssemblyNameEqualityComparer.Instance);
CheckForNonPublicTypes(typeInfo, assembliesDeclaringInternalTypes, visitedTypes);
// Enumerate members on the interface that we're going to need to implement.
@@ -256,35 +256,4 @@ private TypeInfo EmitMagicAttribute()
return tb.CreateTypeInfo()!;
}
-
- private class AssemblyNameEqualityComparer : IEqualityComparer
- {
- internal static readonly IEqualityComparer Instance = new AssemblyNameEqualityComparer();
-
- private AssemblyNameEqualityComparer()
- {
- }
-
- public bool Equals(AssemblyName? x, AssemblyName? y)
- {
- if (x is null && y is null)
- {
- return true;
- }
-
- if (x is null || y is null)
- {
- return false;
- }
-
- return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase);
- }
-
- public int GetHashCode(AssemblyName obj)
- {
- Requires.NotNull(obj, nameof(obj));
-
- return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullName);
- }
- }
}
diff --git a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj
index 6b480336d..ae705836b 100644
--- a/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj
+++ b/test/StreamJsonRpc.Tests.NoInterceptors/StreamJsonRpc.Tests.NoInterceptors.csproj
@@ -15,6 +15,7 @@
+
@@ -41,6 +42,7 @@
+
false
diff --git a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs
index 3cbfa22fd..b8938e7d9 100644
--- a/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs
+++ b/test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs
@@ -5,9 +5,13 @@
using System.Diagnostics;
using System.Reflection;
+#if NET
+using System.Runtime.Loader;
+#endif
using Microsoft.VisualStudio.Threading;
using Nerdbank;
using StreamJsonRpc.Reflection;
+using StreamJsonRpc.Tests;
using ExAssembly = StreamJsonRpc.Tests.ExternalAssembly;
public abstract partial class JsonRpcProxyGenerationTests : TestBase
@@ -162,6 +166,12 @@ public interface IServerWithGenericMethod
Task AddAsync(T a, T b);
}
+ [JsonRpcContract]
+ public partial interface IReferenceAnUnreachableAssembly
+ {
+ Task TakeAsync(UnreachableAssembly.SomeUnreachableClass obj);
+ }
+
[JsonRpcContract]
internal partial interface IServerInternal :
ExAssembly.ISomeInternalProxyInterface,
@@ -880,6 +890,44 @@ public async Task ValueTaskReturningMethod()
await clientRpc.DoSomethingValueAsync();
}
+ ///
+ /// Validates that similar proxies are generated in the same dynamic assembly.
+ ///
+ [Fact]
+ public void ReuseDynamicAssembliesTest()
+ {
+ JsonRpc clientRpc = new(Stream.Null);
+ IServer proxy1 = clientRpc.Attach(this.DefaultProxyOptions);
+ IServer2 proxy2 = clientRpc.Attach(this.DefaultProxyOptions);
+ Assert.Same(proxy1.GetType().Assembly, proxy2.GetType().Assembly);
+ }
+
+#if NET
+ [Fact]
+ public void DynamicAssembliesKeyedByAssemblyLoadContext()
+ {
+ UnreachableAssemblyTools.VerifyUnreachableAssembly();
+
+ // Set up a new ALC that can find the hidden assembly, and ask for the proxy type.
+ AssemblyLoadContext alc = UnreachableAssemblyTools.CreateContextForReachingTheUnreachable();
+
+ JsonRpc clientRpc = new(Stream.Null);
+
+ // Ensure we first generate a proxy in our own default ALC.
+ // The goal being to emit a DynamicAssembly that we *might* reuse
+ // for the later proxy for which the first DynamicAssembly is not appropriate.
+ clientRpc.Attach(this.DefaultProxyOptions);
+
+ // Now take very specific steps to invoke the rest of the test in the other AssemblyLoadContext.
+ // This is important so that our IReferenceAnUnreachableAssembly type will be able to resolve its
+ // own type references to UnreachableAssembly.dll, which our own default ALC cannot do.
+ MethodInfo helperMethodInfo = typeof(JsonRpcProxyGenerationTests).GetMethod(nameof(DynamicAssembliesKeyedByAssemblyLoadContext_Helper), BindingFlags.NonPublic | BindingFlags.Static)!;
+ MethodInfo helperWithinAlc = UnreachableAssemblyTools.LoadHelperInAlc(alc, helperMethodInfo);
+ helperWithinAlc.Invoke(null, [this.DefaultProxyOptions]);
+ }
+
+#endif
+
protected T AttachJsonRpc(Stream stream)
where T : class
{
@@ -889,6 +937,23 @@ protected T AttachJsonRpc(Stream stream)
return proxy;
}
+#if NET
+
+ private static void DynamicAssembliesKeyedByAssemblyLoadContext_Helper(JsonRpcProxyOptions options)
+ {
+ // Although this method executes within the special ALC,
+ // StreamJsonRpc is loaded in the default ALC.
+ // Therefore unless StreamJsonRpc is taking care to use a DynamicAssembly
+ // that belongs to *this* ALC, it won't be able to resolve the same type references
+ // that we can here (the ones from UnreachableAssembly).
+ // That's what makes this test effective: it'll fail if the DynamicAssembly is shared across ALCs,
+ // thereby verifying that StreamJsonRpc has a dedicated set of DynamicAssemblies for each ALC.
+ JsonRpc clientRpc = new(Stream.Null);
+ clientRpc.Attach(options);
+ }
+
+#endif
+
#if NO_INTERCEPTORS
public class Dynamic(ITestOutputHelper logger) : JsonRpcProxyGenerationTests(logger, JsonRpcProxyOptions.ProxyImplementation.AlwaysDynamic);
#else
diff --git a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj
index 80dd6c217..3fb23614a 100644
--- a/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj
+++ b/test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj
@@ -81,5 +81,6 @@
+
diff --git a/test/StreamJsonRpc.Tests/UnreachableAssemblyTools.cs b/test/StreamJsonRpc.Tests/UnreachableAssemblyTools.cs
new file mode 100644
index 000000000..bae11d4ec
--- /dev/null
+++ b/test/StreamJsonRpc.Tests/UnreachableAssemblyTools.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#if NET
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
+
+namespace StreamJsonRpc.Tests;
+
+internal static class UnreachableAssemblyTools
+{
+ ///
+ /// Useful for tests to call before asserting conditions that depend on the UnreachableAssembly.dll
+ /// actually being unreachable.
+ ///
+ internal static void VerifyUnreachableAssembly()
+ {
+ Assert.Throws(() => typeof(UnreachableAssembly.SomeUnreachableClass));
+ }
+
+ ///
+ /// Initializes an with UnreachableAssembly.dll loaded into it.
+ ///
+ /// The name to give the .
+ /// The new .
+ internal static AssemblyLoadContext CreateContextForReachingTheUnreachable([CallerMemberName] string? testName = null)
+ {
+ AssemblyLoadContext alc = new(testName);
+ alc.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "hidden", "UnreachableAssembly.dll"));
+ return alc;
+ }
+
+ ///
+ /// Translates a from one ALC into another ALC, so that it can be invoked
+ /// within the context of the new ALC.
+ ///
+ /// The to load the method into.
+ /// The of the method in the caller's ALC to load into the given .
+ /// The translated .
+ internal static MethodInfo LoadHelperInAlc(AssemblyLoadContext alc, MethodInfo helperMethodInfo)
+ {
+ Assembly selfWithinAlc = alc.LoadFromAssemblyPath(helperMethodInfo.DeclaringType!.Assembly.Location);
+ MethodInfo helperWithinAlc = (MethodInfo)selfWithinAlc.ManifestModule.ResolveMethod(helperMethodInfo.MetadataToken)!;
+ return helperWithinAlc;
+ }
+}
+
+#endif
diff --git a/test/UnreachableAssembly.targets b/test/UnreachableAssembly.targets
new file mode 100644
index 000000000..ae09558fb
--- /dev/null
+++ b/test/UnreachableAssembly.targets
@@ -0,0 +1,14 @@
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
diff --git a/test/UnreachableAssembly/SomeUnreachableClass.cs b/test/UnreachableAssembly/SomeUnreachableClass.cs
new file mode 100644
index 000000000..013112061
--- /dev/null
+++ b/test/UnreachableAssembly/SomeUnreachableClass.cs
@@ -0,0 +1,5 @@
+namespace UnreachableAssembly;
+
+public class SomeUnreachableClass
+{
+}
diff --git a/test/UnreachableAssembly/UnreachableAssembly.csproj b/test/UnreachableAssembly/UnreachableAssembly.csproj
new file mode 100644
index 000000000..089efa6ca
--- /dev/null
+++ b/test/UnreachableAssembly/UnreachableAssembly.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net8.0
+ $(TargetFrameworks);net472
+
+
+