Skip to content

Commit 5a32ad3

Browse files
committed
Default to creating dynamic assemblies in the ALC that loaded the first interface
1 parent 978f8ea commit 5a32ad3

File tree

3 files changed

+28
-19
lines changed

3 files changed

+28
-19
lines changed

docfx/docs/dynamicproxy.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,24 @@ The dynamic proxy maintains the same async-only contract that is exposed by the
5353

5454
## AssemblyLoadContext considerations
5555

56-
When in a .NET process with multiple <xref:System.Runtime.Loader.AssemblyLoadContext> instances, you should consider whether StreamJsonRpc is loaded in an <xref:System.Runtime.Loader.AssemblyLoadContext> that can load all the types required by the proxy interface.
56+
When in a .NET process with multiple <xref:System.Runtime.Loader.AssemblyLoadContext> (ALC) instances, you should consider whether StreamJsonRpc is loaded in an ALC that can load all the types required by the proxy interface.
5757

58-
By default, StreamJsonRpc will generate dynamic proxies in the <xref:System.Runtime.Loader.AssemblyLoadContext> that StreamJsonRpc is loaded within.
59-
This means that if your own code is running in a different <xref:System.Runtime.Loader.AssemblyLoadContext> from StreamJsonRpc and ask for a proxy, the proxy may fail to activate from a type load failure even if your calling code *can* or has loaded that type.
60-
It might also manifest as an <xref:System.MissingMethodException> or <xref:System.InvalidCastException> due to types loading into multiple <xref:System.Runtime.Loader.AssemblyLoadContext> instances.
58+
By default, StreamJsonRpc will generate dynamic proxies in the ALC that the (first) interface requested for the proxy is loaded within.
59+
This is usually the right choice because the interface should be in an ALC that can resolve all the interface's type references.
60+
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.
61+
The need to control this may manifest as an <xref:System.MissingMethodException> or <xref:System.InvalidCastException> due to types loading into multiple ALC instances.
6162

62-
In such cases, you may control the <xref:System.Runtime.Loader.AssemblyLoadContext> used to generate the proxy by surrounding your proxy request with a call to <xref:System.Runtime.Loader.AssemblyLoadContext.EnterContextualReflection*> (and disposal of its result).
63+
In such cases, you may control the ALC used to generate the proxy by surrounding your proxy request with a call to <xref:System.Runtime.Loader.AssemblyLoadContext.EnterContextualReflection*> (and disposal of its result).
6364

64-
For example, you might use the following code when StreamJsonRpc is loaded into a different <xref:System.Runtime.Loader.AssemblyLoadContext> from your own code:
65+
For example, you might use the following code when StreamJsonRpc is loaded into a different ALC from your own code:
6566

6667
```cs
67-
IMyService proxy;
68-
using (AssemblyLoadContext.EnterContextualReflection(MethodBase.GetCurrentMethod()!.DeclaringType!.Assembly))
68+
// Whatever ALC can resolve *all* type references in *all* proxy interfaces.
69+
AssemblyLoadContext alc = AssemblyLoadContext.GetLoadContext(MethodBase.GetCurrentMethod()!.DeclaringType!.Assembly);
70+
IFoo proxy;
71+
using (AssemblyLoadContext.EnterContextualReflection(alc))
6972
{
70-
proxy = jsonRpc.Attach<IMyService>();
73+
proxy = (IFoo)jsonRpc.Attach([typeof(IFoo), typeof(IFoo2)]);
7174
}
7275
```
7376

src/StreamJsonRpc/ProxyGeneration.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan<Type> addition
111111
// Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations.
112112
SortRpcInterfaces(rpcInterfaces);
113113

114+
// For ALC selection reasons, it's vital that the *user's* selected interfaces come *before* our own supporting interfaces.
114115
Type[] proxyInterfaces = [.. rpcInterfaces.Select(i => i.Type), typeof(IJsonRpcClientProxy), typeof(IJsonRpcClientProxyInternal)];
115116
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(proxyInterfaces);
116117

@@ -764,7 +765,11 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
764765
#if NET
765766
// We have to key the dynamic assembly by ALC as well, since callers may set a custom contextual reflection context
766767
// that influences how the assembly will resolve its type references.
767-
AssemblyLoadContext alc = AssemblyLoadContext.CurrentContextualReflectionContext ?? AssemblyLoadContext.GetLoadContext(typeof(ProxyGeneration).Assembly) ?? throw new Exception("No ALC for our own assembly!");
768+
// If they haven't set a contextual one, we assume the ALC that defines the (first) proxy interface.
769+
AssemblyLoadContext alc = AssemblyLoadContext.CurrentContextualReflectionContext
770+
?? AssemblyLoadContext.GetLoadContext(interfaceTypes[0].Assembly)
771+
?? AssemblyLoadContext.GetLoadContext(typeof(ProxyGeneration).Assembly)
772+
?? throw new Exception("No ALC for our own assembly!");
768773
foreach ((AssemblyLoadContext AssemblyLoadContext, ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
769774
{
770775
if (existingSet.AssemblyLoadContext != alc)
@@ -787,7 +792,14 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
787792
// I have disabled this optimization though till we need it since it would sometimes cover up any bugs in the above visibility checking code.
788793
////skipVisibilityCheckAssemblies = skipVisibilityCheckAssemblies.Union(AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName()));
789794

790-
AssemblyBuilder assemblyBuilder = CreateProxyAssemblyBuilder();
795+
AssemblyBuilder assemblyBuilder;
796+
#if NET
797+
using (alc.EnterContextualReflection())
798+
#endif
799+
{
800+
assemblyBuilder = CreateProxyAssemblyBuilder();
801+
}
802+
791803
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("rpcProxies");
792804
var skipClrVisibilityChecks = new SkipClrVisibilityChecks(assemblyBuilder, moduleBuilder);
793805
skipClrVisibilityChecks.SkipVisibilityChecksFor(skipVisibilityCheckAssemblies);

test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -853,14 +853,8 @@ private static void DynamicAssembliesKeyedByAssemblyLoadContext_Helper()
853853
// that we can here (the ones from UnreachableAssembly).
854854
// That's what makes this test effective: it'll fail if the DynamicAssembly is shared across ALCs,
855855
// thereby verifying that StreamJsonRpc has a dedicated set of DynamicAssemblies for each ALC.
856-
// We have to manually set the contextual reflection context to this special ALC
857-
// so that StreamJsonRpc knows to get the DynamicAssembly for this ALC.
858-
// Otherwise it will default to its own.
859-
using (AssemblyLoadContext.EnterContextualReflection(MethodBase.GetCurrentMethod()!.DeclaringType!.Assembly))
860-
{
861-
JsonRpc clientRpc = new(Stream.Null);
862-
clientRpc.Attach<IReferenceAnUnreachableAssembly>();
863-
}
856+
JsonRpc clientRpc = new(Stream.Null);
857+
clientRpc.Attach<IReferenceAnUnreachableAssembly>();
864858
}
865859

866860
#endif

0 commit comments

Comments
 (0)