Skip to content

Commit 6b2434e

Browse files
authored
Merge pull request #1247 from microsoft/dev/andarno/fixDynamicAssemblies
Create dynamic proxies in the same ALC as the interface they implement
2 parents 7d3021c + c1ed6ae commit 6b2434e

File tree

11 files changed

+246
-37
lines changed

11 files changed

+246
-37
lines changed

StreamJsonRpc.sln

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.0.31707.426
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.0.10912.84 main
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreamJsonRpc", "src\StreamJsonRpc\StreamJsonRpc.csproj", "{DFBD1BCA-EAE0-4454-9E97-FA9BD9A0F03A}"
77
EndProject
@@ -38,6 +38,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F446B894-5
3838
test\Directory.Build.targets = test\Directory.Build.targets
3939
EndProjectSection
4040
EndProject
41+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnreachableAssembly", "test\UnreachableAssembly\UnreachableAssembly.csproj", "{5AAF7DDA-6CC0-456B-A7E1-B33893915662}"
42+
EndProject
4143
Global
4244
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4345
Debug|Any CPU = Debug|Any CPU
@@ -60,6 +62,10 @@ Global
6062
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Debug|Any CPU.Build.0 = Debug|Any CPU
6163
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Release|Any CPU.ActiveCfg = Release|Any CPU
6264
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Release|Any CPU.Build.0 = Release|Any CPU
65+
{5AAF7DDA-6CC0-456B-A7E1-B33893915662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
66+
{5AAF7DDA-6CC0-456B-A7E1-B33893915662}.Debug|Any CPU.Build.0 = Debug|Any CPU
67+
{5AAF7DDA-6CC0-456B-A7E1-B33893915662}.Release|Any CPU.ActiveCfg = Release|Any CPU
68+
{5AAF7DDA-6CC0-456B-A7E1-B33893915662}.Release|Any CPU.Build.0 = Release|Any CPU
6369
EndGlobalSection
6470
GlobalSection(SolutionProperties) = preSolution
6571
HideSolutionNode = FALSE
@@ -69,6 +75,7 @@ Global
6975
{8BF355B2-E3B0-4615-BFC1-7563EADC4F8B} = {F446B894-56AA-4653-ADC0-5FFC911C9C13}
7076
{CEF0F77F-19EB-4C76-A050-854984BB0364} = {F446B894-56AA-4653-ADC0-5FFC911C9C13}
7177
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215} = {F446B894-56AA-4653-ADC0-5FFC911C9C13}
78+
{5AAF7DDA-6CC0-456B-A7E1-B33893915662} = {F446B894-56AA-4653-ADC0-5FFC911C9C13}
7279
EndGlobalSection
7380
GlobalSection(ExtensibilityGlobals) = postSolution
7481
SolutionGuid = {4946F7E7-0619-414B-BE56-DDF0261CA8A9}

azure-pipelines/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ parameters:
2121
# This is just one of a a few mechanisms to enforce code style consistency.
2222
- name: EnableDotNetFormatCheck
2323
type: boolean
24-
default: true
24+
default: false # disable in v2.22 because it's defective (https://github.com/dotnet/sdk/issues/50262)
2525
# This lists the names of the artifacts that will be published *from every OS build agent*.
2626
# Any new tools/artifacts/*.ps1 script needs to be added to this list.
2727
# If an artifact is only generated or collected on one OS, it should NOT be listed here,

docfx/docs/dynamicproxy.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,28 @@ between client and server.
5050
Sometimes a client may need to block its caller until a response to a JSON-RPC request comes back.
5151
The dynamic proxy maintains the same async-only contract that is exposed by the @StreamJsonRpc.JsonRpc class itself.
5252
[Learn more about sending requests](sendrequest.md), particularly under the heading about async responses.
53+
54+
## AssemblyLoadContext considerations
55+
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.
57+
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.
62+
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).
64+
65+
For example, you might use the following code when StreamJsonRpc is loaded into a different ALC from your own code:
66+
67+
```cs
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))
72+
{
73+
proxy = (IFoo)jsonRpc.Attach([typeof(IFoo), typeof(IFoo2)]);
74+
}
75+
```
76+
77+
This initializes the `proxy` local variable with a proxy that will be able to load all types that your own <xref:System.Runtime.Loader.AssemblyLoadContext> can load.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
3+
using System.Reflection;
4+
5+
namespace StreamJsonRpc;
6+
7+
internal class AssemblyNameEqualityComparer : IEqualityComparer<AssemblyName>
8+
{
9+
internal static readonly IEqualityComparer<AssemblyName> Instance = new AssemblyNameEqualityComparer();
10+
11+
private AssemblyNameEqualityComparer()
12+
{
13+
}
14+
15+
public bool Equals(AssemblyName? x, AssemblyName? y)
16+
{
17+
if (x is null && y is null)
18+
{
19+
return true;
20+
}
21+
22+
if (x is null || y is null)
23+
{
24+
return false;
25+
}
26+
27+
return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase);
28+
}
29+
30+
public int GetHashCode(AssemblyName obj)
31+
{
32+
Requires.NotNull(obj, nameof(obj));
33+
34+
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullName);
35+
}
36+
}

src/StreamJsonRpc/ProxyGeneration.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
using System.Globalization;
66
using System.Reflection;
77
using System.Reflection.Emit;
8+
#if NET
9+
using System.Runtime.Loader;
10+
#endif
811
using Microsoft.VisualStudio.Threading;
912
using StreamJsonRpc.Reflection;
1013
using CodeGenHelpers = StreamJsonRpc.Reflection.CodeGenHelpers;
@@ -18,7 +21,11 @@ namespace StreamJsonRpc;
1821

1922
internal static class ProxyGeneration
2023
{
24+
#if NET
25+
private static readonly List<(AssemblyLoadContext, ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder)> TransparentProxyModuleBuilderByVisibilityCheck = [];
26+
#else
2127
private static readonly List<(ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder)> TransparentProxyModuleBuilderByVisibilityCheck = new List<(ImmutableHashSet<AssemblyName>, ModuleBuilder)>();
28+
#endif
2229
private static readonly object BuilderLock = new object();
2330
private static readonly AssemblyName ProxyAssemblyName = new AssemblyName(string.Format(CultureInfo.InvariantCulture, "StreamJsonRpc_Proxies_{0}", Guid.NewGuid()));
2431
private static readonly MethodInfo DelegateCombineMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Combine), new Type[] { typeof(Delegate), typeof(Delegate) })!;
@@ -104,6 +111,9 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan<Type> addition
104111
// Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations.
105112
SortRpcInterfaces(rpcInterfaces);
106113

114+
// For ALC selection reasons, it's vital that the *user's* selected interfaces come *before* our own supporting interfaces.
115+
// If the order is incorrect, type resolution may fail or the wrong AssemblyLoadContext (ALC) may be selected,
116+
// leading to runtime errors or unexpected behavior when loading types or invoking methods.
107117
Type[] proxyInterfaces = [.. rpcInterfaces.Select(i => i.Type), typeof(IJsonRpcClientProxy), typeof(IJsonRpcClientProxyInternal)];
108118
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(proxyInterfaces);
109119

@@ -752,10 +762,27 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
752762
// For each set of skip visibility check assemblies, we need a dynamic assembly that skips at *least* that set.
753763
// The CLR will not honor any additions to that set once the first generated type is closed.
754764
// We maintain a dictionary to point at dynamic modules based on the set of skip visibility check assemblies they were generated with.
755-
ImmutableHashSet<AssemblyName> skipVisibilityCheckAssemblies = ImmutableHashSet.CreateRange(interfaceTypes.SelectMany(t => SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(t.GetTypeInfo())))
765+
ImmutableHashSet<AssemblyName> skipVisibilityCheckAssemblies = ImmutableHashSet.CreateRange(AssemblyNameEqualityComparer.Instance, interfaceTypes.SelectMany(t => SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(t.GetTypeInfo())))
756766
.Add(typeof(ProxyGeneration).Assembly.GetName());
767+
#if NET
768+
// We have to key the dynamic assembly by ALC as well, since callers may set a custom contextual reflection context
769+
// that influences how the assembly will resolve its type references.
770+
// If they haven't set a contextual one, we assume the ALC that defines the (first) proxy interface.
771+
AssemblyLoadContext alc = AssemblyLoadContext.CurrentContextualReflectionContext
772+
?? AssemblyLoadContext.GetLoadContext(interfaceTypes[0].Assembly)
773+
?? AssemblyLoadContext.GetLoadContext(typeof(ProxyGeneration).Assembly)
774+
?? throw new Exception("No ALC for our own assembly!");
775+
foreach ((AssemblyLoadContext AssemblyLoadContext, ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
776+
{
777+
if (existingSet.AssemblyLoadContext != alc)
778+
{
779+
continue;
780+
}
781+
782+
#else
757783
foreach ((ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
758784
{
785+
#endif
759786
if (existingSet.SkipVisibilitySet.IsSupersetOf(skipVisibilityCheckAssemblies))
760787
{
761788
return existingSet.Builder;
@@ -767,11 +794,22 @@ private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
767794
// I have disabled this optimization though till we need it since it would sometimes cover up any bugs in the above visibility checking code.
768795
////skipVisibilityCheckAssemblies = skipVisibilityCheckAssemblies.Union(AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName()));
769796

770-
AssemblyBuilder assemblyBuilder = CreateProxyAssemblyBuilder();
797+
AssemblyBuilder assemblyBuilder;
798+
#if NET
799+
using (alc.EnterContextualReflection())
800+
#endif
801+
{
802+
assemblyBuilder = CreateProxyAssemblyBuilder();
803+
}
804+
771805
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("rpcProxies");
772806
var skipClrVisibilityChecks = new SkipClrVisibilityChecks(assemblyBuilder, moduleBuilder);
773807
skipClrVisibilityChecks.SkipVisibilityChecksFor(skipVisibilityCheckAssemblies);
808+
#if NET
809+
TransparentProxyModuleBuilderByVisibilityCheck.Add((alc, skipVisibilityCheckAssemblies, moduleBuilder));
810+
#else
774811
TransparentProxyModuleBuilderByVisibilityCheck.Add((skipVisibilityCheckAssemblies, moduleBuilder));
812+
#endif
775813

776814
return moduleBuilder;
777815
}

src/StreamJsonRpc/SkipClrVisibilityChecks.cs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ internal static ImmutableHashSet<AssemblyName> GetSkipVisibilityChecksRequiremen
7979
Requires.NotNull(typeInfo, nameof(typeInfo));
8080

8181
var visitedTypes = new HashSet<TypeInfo>();
82-
ImmutableHashSet<AssemblyName>.Builder assembliesDeclaringInternalTypes = ImmutableHashSet.CreateBuilder<AssemblyName>(AssemblyNameEqualityComparer.Instance);
82+
ImmutableHashSet<AssemblyName>.Builder assembliesDeclaringInternalTypes = ImmutableHashSet.CreateBuilder(AssemblyNameEqualityComparer.Instance);
8383
CheckForNonPublicTypes(typeInfo, assembliesDeclaringInternalTypes, visitedTypes);
8484

8585
// Enumerate members on the interface that we're going to need to implement.
@@ -253,35 +253,4 @@ private TypeInfo EmitMagicAttribute()
253253

254254
return tb.CreateTypeInfo()!;
255255
}
256-
257-
private class AssemblyNameEqualityComparer : IEqualityComparer<AssemblyName>
258-
{
259-
internal static readonly IEqualityComparer<AssemblyName> Instance = new AssemblyNameEqualityComparer();
260-
261-
private AssemblyNameEqualityComparer()
262-
{
263-
}
264-
265-
public bool Equals(AssemblyName? x, AssemblyName? y)
266-
{
267-
if (x is null && y is null)
268-
{
269-
return true;
270-
}
271-
272-
if (x is null || y is null)
273-
{
274-
return false;
275-
}
276-
277-
return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase);
278-
}
279-
280-
public int GetHashCode(AssemblyName obj)
281-
{
282-
Requires.NotNull(obj, nameof(obj));
283-
284-
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FullName);
285-
}
286-
}
287256
}

test/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
#if NET
5+
using System.Reflection;
6+
using System.Runtime.Loader;
7+
#endif
48
using Microsoft.VisualStudio.Threading;
59
using Nerdbank;
10+
using StreamJsonRpc.Tests;
611
using ExAssembly = StreamJsonRpc.Tests.ExternalAssembly;
712

813
public class JsonRpcProxyGenerationTests : TestBase
@@ -135,6 +140,11 @@ public interface IServerWithGenericMethod
135140
Task AddAsync<T>(T a, T b);
136141
}
137142

143+
public interface IReferenceAnUnreachableAssembly
144+
{
145+
Task TakeAsync(UnreachableAssembly.SomeUnreachableClass obj);
146+
}
147+
138148
internal interface IServerInternal :
139149
ExAssembly.ISomeInternalProxyInterface,
140150
IServerInternalWithInternalTypesFromOtherAssemblies,
@@ -798,6 +808,57 @@ public async Task ValueTaskReturningMethod()
798808
await clientRpc.DoSomethingValueAsync();
799809
}
800810

811+
/// <summary>
812+
/// Validates that similar proxies are generated in the same dynamic assembly.
813+
/// </summary>
814+
[Fact]
815+
public void ReuseDynamicAssembliesTest()
816+
{
817+
JsonRpc clientRpc = new(Stream.Null);
818+
IServer proxy1 = clientRpc.Attach<IServer>();
819+
IServer2 proxy2 = clientRpc.Attach<IServer2>();
820+
Assert.Same(proxy1.GetType().Assembly, proxy2.GetType().Assembly);
821+
}
822+
823+
#if NET
824+
[Fact]
825+
public void DynamicAssembliesKeyedByAssemblyLoadContext()
826+
{
827+
UnreachableAssemblyTools.VerifyUnreachableAssembly();
828+
829+
// Set up a new ALC that can find the hidden assembly, and ask for the proxy type.
830+
AssemblyLoadContext alc = UnreachableAssemblyTools.CreateContextForReachingTheUnreachable();
831+
832+
JsonRpc clientRpc = new(Stream.Null);
833+
834+
// Ensure we first generate a proxy in our own default ALC.
835+
// The goal being to emit a DynamicAssembly that we *might* reuse
836+
// for the later proxy for which the first DynamicAssembly is not appropriate.
837+
clientRpc.Attach<IServer>();
838+
839+
// Now take very specific steps to invoke the rest of the test in the other AssemblyLoadContext.
840+
// This is important so that our IReferenceAnUnreachableAssembly type will be able to resolve its
841+
// own type references to UnreachableAssembly.dll, which our own default ALC cannot do.
842+
MethodInfo helperMethodInfo = typeof(JsonRpcProxyGenerationTests).GetMethod(nameof(DynamicAssembliesKeyedByAssemblyLoadContext_Helper), BindingFlags.NonPublic | BindingFlags.Static)!;
843+
MethodInfo helperWithinAlc = UnreachableAssemblyTools.LoadHelperInAlc(alc, helperMethodInfo);
844+
helperWithinAlc.Invoke(null, null);
845+
}
846+
847+
private static void DynamicAssembliesKeyedByAssemblyLoadContext_Helper()
848+
{
849+
// Although this method executes within the special ALC,
850+
// StreamJsonRpc is loaded in the default ALC.
851+
// Therefore unless StreamJsonRpc is taking care to use a DynamicAssembly
852+
// that belongs to *this* ALC, it won't be able to resolve the same type references
853+
// that we can here (the ones from UnreachableAssembly).
854+
// That's what makes this test effective: it'll fail if the DynamicAssembly is shared across ALCs,
855+
// thereby verifying that StreamJsonRpc has a dedicated set of DynamicAssemblies for each ALC.
856+
JsonRpc clientRpc = new(Stream.Null);
857+
clientRpc.Attach<IReferenceAnUnreachableAssembly>();
858+
}
859+
860+
#endif
861+
801862
public class EmptyClass
802863
{
803864
}

test/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
</ItemGroup>
4848
<ItemGroup>
4949
<ProjectReference Include="..\StreamJsonRpc.Tests.ExternalAssembly\StreamJsonRpc.Tests.ExternalAssembly.csproj" />
50+
<ProjectReference Include="..\UnreachableAssembly\UnreachableAssembly.csproj">
51+
<!-- We MUST NOT have this assembly in our test assembly's output directory so that it cannot be reached from the default ALC. -->
52+
<Private>false</Private>
53+
</ProjectReference>
5054
<ProjectReference Include="..\..\src\StreamJsonRpc\StreamJsonRpc.csproj" />
5155
</ItemGroup>
5256
<ItemGroup>
@@ -68,4 +72,10 @@
6872
<ItemGroup>
6973
<Reference Include="Microsoft.CSharp" Condition=" '$(TargetFramework)' == 'net472' " />
7074
</ItemGroup>
75+
<Target Name="PlaceMissingAssembly" AfterTargets="ResolveReferences">
76+
<ItemGroup>
77+
<DivertedProjectReferenceOutputs Include="@(ReferencePath)" Condition=" '%(FileName)' == 'UnreachableAssembly' " />
78+
</ItemGroup>
79+
<Copy SourceFiles="@(DivertedProjectReferenceOutputs)" DestinationFolder="$(OutputPath)hidden" SkipUnchangedFiles="true" />
80+
</Target>
7181
</Project>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
#if NET
5+
6+
using System.Reflection;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.Loader;
9+
10+
namespace StreamJsonRpc.Tests;
11+
12+
internal static class UnreachableAssemblyTools
13+
{
14+
/// <summary>
15+
/// Useful for tests to call before asserting conditions that depend on the UnreachableAssembly.dll
16+
/// actually being unreachable.
17+
/// </summary>
18+
internal static void VerifyUnreachableAssembly()
19+
{
20+
Assert.Throws<FileNotFoundException>(() => typeof(UnreachableAssembly.SomeUnreachableClass));
21+
}
22+
23+
/// <summary>
24+
/// Initializes an <see cref="AssemblyLoadContext"/> with UnreachableAssembly.dll loaded into it.
25+
/// </summary>
26+
/// <param name="testName">The name to give the <see cref="AssemblyLoadContext"/>.</param>
27+
/// <returns>The new <see cref="AssemblyLoadContext"/>.</returns>
28+
internal static AssemblyLoadContext CreateContextForReachingTheUnreachable([CallerMemberName] string? testName = null)
29+
{
30+
AssemblyLoadContext alc = new(testName);
31+
alc.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "hidden", "UnreachableAssembly.dll"));
32+
return alc;
33+
}
34+
35+
/// <summary>
36+
/// Translates a <see cref="MethodInfo"/> from one ALC into another ALC, so that it can be invoked
37+
/// within the context of the new ALC.
38+
/// </summary>
39+
/// <param name="alc">The <see cref="AssemblyLoadContext"/> to load the method into.</param>
40+
/// <param name="helperMethodInfo">The <see cref="MethodInfo"/> of the method in the caller's ALC to load into the given <paramref name="alc"/>.</param>
41+
/// <returns>The translated <see cref="MethodInfo"/>.</returns>
42+
internal static MethodInfo LoadHelperInAlc(AssemblyLoadContext alc, MethodInfo helperMethodInfo)
43+
{
44+
Assembly selfWithinAlc = alc.LoadFromAssemblyPath(helperMethodInfo.DeclaringType!.Assembly.Location);
45+
MethodInfo helperWithinAlc = (MethodInfo)selfWithinAlc.ManifestModule.ResolveMethod(helperMethodInfo.MetadataToken)!;
46+
return helperWithinAlc;
47+
}
48+
}
49+
50+
#endif

0 commit comments

Comments
 (0)