diff --git a/protobuf-net.Grpc.sln b/protobuf-net.Grpc.sln index ece2389..1e5bd46 100644 --- a/protobuf-net.Grpc.sln +++ b/protobuf-net.Grpc.sln @@ -143,6 +143,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TraderSys.Portfolios.Shared EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{9479D65D-A0E8-4520-99EC-93774A217EF6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "protobuf-net.Grpc.Tests.TestProxy", "src\protobuf-net.Grpc.Tests.TestProxy\protobuf-net.Grpc.Tests.TestProxy.csproj", "{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -402,6 +411,12 @@ Global {9479D65D-A0E8-4520-99EC-93774A217EF6}.Release|Any CPU.Build.0 = Release|Any CPU {9479D65D-A0E8-4520-99EC-93774A217EF6}.VS|Any CPU.ActiveCfg = Debug|Any CPU {9479D65D-A0E8-4520-99EC-93774A217EF6}.VS|Any CPU.Build.0 = Debug|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Release|Any CPU.Build.0 = Release|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.VS|Any CPU.ActiveCfg = Debug|Any CPU + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.VS|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -463,6 +478,7 @@ Global {AC65761B-5CF5-4F3E-AF60-41F977E8070D} = {2B3F5AED-24B3-4915-900E-EFC58414EA29} {80479131-FE55-473E-AE68-55601AD96668} = {2B3F5AED-24B3-4915-900E-EFC58414EA29} {9479D65D-A0E8-4520-99EC-93774A217EF6} = {8D5FDB6F-6111-4604-8EDF-6A59F8B12D7E} + {69A6D227-F8C8-AED8-8C94-31F9A3A005C4} = {0A84599D-2CE9-416E-888F-24652EEAB0B3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BA14B07C-CA29-430D-A600-F37A050636D3} diff --git a/src/protobuf-net.Grpc.Tests.TestProxy/IProxy.cs b/src/protobuf-net.Grpc.Tests.TestProxy/IProxy.cs new file mode 100644 index 0000000..16c5e72 --- /dev/null +++ b/src/protobuf-net.Grpc.Tests.TestProxy/IProxy.cs @@ -0,0 +1,18 @@ +using ProtoBuf; +using ProtoBuf.Grpc; +using ProtoBuf.Grpc.Configuration; + +namespace protobuf_net.Grpc.Tests.TestProxy; + +[Service] +public interface IProxy +{ + [Operation] + ValueTask Operation(In request, CallContext callContext = default); +} + +[ProtoContract] +public class In { } + +[ProtoContract] +public class Out { } diff --git a/src/protobuf-net.Grpc.Tests.TestProxy/ProxyFactory.cs b/src/protobuf-net.Grpc.Tests.TestProxy/ProxyFactory.cs new file mode 100644 index 0000000..e468308 --- /dev/null +++ b/src/protobuf-net.Grpc.Tests.TestProxy/ProxyFactory.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Runtime.Loader; +using ProtoBuf.Grpc.ClientFactory; +using Grpc.Net.ClientFactory; +using Microsoft.Extensions.DependencyInjection; + +namespace protobuf_net.Grpc.Tests.TestProxy; +public static class ProxyFactory +{ + public static IProxy Create() + { + var services = new ServiceCollection(); + services.AddGrpcClient(o => + { + o.Address = new Uri("http://localhost"); + }).ConfigureCodeFirstGrpcClient(); + var serviceProvider = services.BuildServiceProvider(); + + var clientFactory = serviceProvider.GetRequiredService(); + return clientFactory.CreateClient(nameof(IProxy)); + } +} diff --git a/src/protobuf-net.Grpc.Tests.TestProxy/protobuf-net.Grpc.Tests.TestProxy.csproj b/src/protobuf-net.Grpc.Tests.TestProxy/protobuf-net.Grpc.Tests.TestProxy.csproj new file mode 100644 index 0000000..855ea33 --- /dev/null +++ b/src/protobuf-net.Grpc.Tests.TestProxy/protobuf-net.Grpc.Tests.TestProxy.csproj @@ -0,0 +1,22 @@ + + + + net6.0;net8.0 + protobuf_net.Grpc.Tests.TestProxy + enable + enable + + + + + + + + + runtime + + + runtime + + + diff --git a/src/protobuf-net.Grpc/Internal/ProxyEmitter.cs b/src/protobuf-net.Grpc/Internal/ProxyEmitter.cs index 3ec577e..58288ad 100644 --- a/src/protobuf-net.Grpc/Internal/ProxyEmitter.cs +++ b/src/protobuf-net.Grpc/Internal/ProxyEmitter.cs @@ -10,16 +10,14 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +#if NET6_0_OR_GREATER +using System.Runtime.Loader; +#endif namespace ProtoBuf.Grpc.Internal { internal static class ProxyEmitter { - private static readonly string ProxyIdentity = typeof(ProxyEmitter).Namespace + ".Proxies"; - - private static readonly ModuleBuilder s_module = AssemblyBuilder.DefineDynamicAssembly( - new AssemblyName(ProxyIdentity), AssemblyBuilderAccess.RunAndCollect).DefineDynamicModule(ProxyIdentity); - private static void Ldc_I4(ILGenerator il, int value) { switch (value) @@ -142,14 +140,25 @@ internal static Func EmitFactory(BinderConfigur { Type baseType = GrpcClientFactory.ClientBaseType; +#if NET6_0_OR_GREATER + var proxyLoadContext = AssemblyLoadContext.GetLoadContext(typeof(TService).Assembly) ?? + AssemblyLoadContext.Default; + // Once we have the ALC for reflection, get or create the module for it. + // Any references will be resolved against the ALC that owns the service interface. + ModuleBuilder moduleBuilder = ProxyModuleHelper.GetOrCreateProxyModule(proxyLoadContext); +#else + ModuleBuilder moduleBuilder = ProxyModuleHelper.MainProxyModule; +#endif + var typeIdentity = ProxyModuleHelper.ProxyModuleIdentity + "." + baseType.Name + "." + typeof(TService).Name + "_Proxy_" + _typeIndex++; + var callInvoker = baseType.GetProperty("CallInvoker", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.GetGetMethod(true); if (callInvoker == null || callInvoker.ReturnType != typeof(CallInvoker) || callInvoker.GetParameters().Length != 0) throw new ArgumentException($"The base-type {baseType} for service-proxy {typeof(TService)} lacks a suitable CallInvoker API"); - lock (s_module) + lock (moduleBuilder) { // private sealed class IFooProxy... - var type = s_module.DefineType(ProxyIdentity + "." + baseType.Name + "." + typeof(TService).Name + "_Proxy_" + _typeIndex++, + var type = moduleBuilder.DefineType(typeIdentity, TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.NotPublic | TypeAttributes.BeforeFieldInit); type.SetParent(baseType); @@ -209,7 +218,7 @@ FieldBuilder Marshaller(Type forType) type.DefineMethodOverride(impl, iMethod); var il = impl.GetILGenerator(); - + // check whether the method belongs to [ServiceInherited] interface var isMethodInherited = iMethod.DeclaringType?.IsDefined(typeof(SubServiceAttribute)) ?? false; @@ -238,7 +247,7 @@ FieldBuilder Marshaller(Type forType) } continue; } - + // in case method belongs to an sub-service interface, we have to find the service contract name inheriting it if (isMethodInherited) { @@ -340,7 +349,7 @@ FieldBuilder Marshaller(Type forType) var finalType = type.CreateType()!; #endif // assign the marshallers and invoke the init - foreach((var field, var name, var instance) in marshallers.Values) + foreach ((var field, var name, var instance) in marshallers.Values) { finalType.GetField(name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public)!.SetValue(null, instance); } @@ -371,7 +380,7 @@ internal static readonly FieldInfo s_CallContext_Default = typeof(CallContext).GetField(nameof(CallContext.Default))!, #pragma warning disable CS0618 // Empty s_Empty_Instance = typeof(Empty).GetField(nameof(Empty.Instance))!, - s_Empty_InstaneTask= typeof(Empty).GetField(nameof(Empty.InstanceTask))!; + s_Empty_InstaneTask = typeof(Empty).GetField(nameof(Empty.InstanceTask))!; #pragma warning restore CS0618 internal static readonly MethodInfo s_CallContext_FromCancellationToken = typeof(CallContext).GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static, null, [typeof(CancellationToken)], null)!; diff --git a/src/protobuf-net.Grpc/Internal/ProxyModuleHelper.cs b/src/protobuf-net.Grpc/Internal/ProxyModuleHelper.cs new file mode 100644 index 0000000..50cb045 --- /dev/null +++ b/src/protobuf-net.Grpc/Internal/ProxyModuleHelper.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using System.Reflection.Emit; +#if NET6_0_OR_GREATER +using System.Collections.Concurrent; +using System.Runtime.Loader; +using System.Threading; +#endif + +namespace ProtoBuf.Grpc.Internal; +internal static class ProxyModuleHelper +{ + static ProxyModuleHelper() { } + public static readonly string ProxyModuleIdentity = typeof(ProxyEmitter).Namespace + ".Proxies"; + +#if NET6_0_OR_GREATER + private static int s_moduleCounter = 0; + private static string GetNextModuleIdentity() + { + return ProxyModuleIdentity + "-" + Interlocked.Increment(ref s_moduleCounter); + } + + private static readonly ConcurrentDictionary _proxyModules = new(); + + public static ModuleBuilder GetOrCreateProxyModule(AssemblyLoadContext assemblyLoadContext) + { + return _proxyModules.GetOrAdd(assemblyLoadContext, key => + { + using var _ = key.EnterContextualReflection(); + var alc = CreateProxyModule(GetNextModuleIdentity()); + key.Unloading += _ => RemoveAssemblyLoadContext(key); + return alc; + }); + } + private static bool RemoveAssemblyLoadContext(AssemblyLoadContext alc) + { + return _proxyModules.TryRemove(alc, out _); + } +#else + + public static readonly ModuleBuilder MainProxyModule = CreateProxyModule(ProxyModuleIdentity); + +#endif + + private static ModuleBuilder CreateProxyModule(string moduleIdentity) + { + var name = new AssemblyName(moduleIdentity); + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect); + return assembly.DefineDynamicModule(moduleIdentity); + } +} diff --git a/tests/protobuf-net.Grpc.Test/AssemblyLoadContextsClientFactoryTests.cs b/tests/protobuf-net.Grpc.Test/AssemblyLoadContextsClientFactoryTests.cs new file mode 100644 index 0000000..38be7c0 --- /dev/null +++ b/tests/protobuf-net.Grpc.Test/AssemblyLoadContextsClientFactoryTests.cs @@ -0,0 +1,117 @@ +#if CLIENT_FACTORY +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Xunit; + +namespace protobuf_net.Grpc.Test; +public class AssemblyLoadContextsClientFactoryTests +{ + private const string TestProxyAssemblyFileName = "protobuf-net.Grpc.Tests.TestProxy.dll"; + private static readonly string CurrentAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + private static readonly string ProxyPath = Path.Combine(CurrentAssemblyFolder, TestProxyAssemblyFileName); + + [Fact] + public void CanCreateProxiesWithPureIsolation() + { + AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder); + AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder); + + AssertPlugins(plugin1, plugin2); + } + + [Fact] + public void CanCreateProxiesWithSharedALC() + { + AssemblyLoadContext shared = CreatePluginAssemblyLoadContext("Shared", CurrentAssemblyFolder); + AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder, shared); + AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder, shared); + + AssertPlugins(plugin1, plugin2); + } + + [Fact] + public void CanCreateProxiesWithSharedPlugin() + { + AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder); + AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder, plugin1); + + AssertPlugins(plugin1, plugin2); + } + + private static void AssertPlugins(AssemblyLoadContext plugin1, AssemblyLoadContext plugin2) + { + var proxyAssembly1 = plugin1.LoadFromAssemblyPath(ProxyPath); + var proxyAssembly2 = plugin2.LoadFromAssemblyPath(ProxyPath); + + object? proxy1 = CreateAndAssertProxy(proxyAssembly1, plugin1); + object? proxy2 = CreateAndAssertProxy(proxyAssembly2, plugin2); + + Assert.NotSame(proxy1!.GetType(), proxy2!.GetType()); + + object? anotherProxy1 = CreateAndAssertProxy(proxyAssembly1, plugin1); + Assert.NotSame(proxy1, anotherProxy1); + Assert.Equal(proxy1.GetType(), anotherProxy1!.GetType()); + + + object? anotherProxy2 = CreateAndAssertProxy(proxyAssembly2, plugin2); + Assert.NotSame(proxy2, anotherProxy2); + Assert.Equal(proxy2.GetType(), anotherProxy2!.GetType()); + } + + private static object? CreateAndAssertProxy(Assembly proxyAssembly, AssemblyLoadContext expectedAssemblyLoadContext) + { + Assert.NotNull(proxyAssembly); + object? proxy = CreateProxy(proxyAssembly); + Assert.NotNull(proxy); + Type pluginType = proxy!.GetType(); + Assert.Equal("IProxy", pluginType.GetInterfaces().First().Name); + Assert.Equal(expectedAssemblyLoadContext, AssemblyLoadContext.GetLoadContext(pluginType.Assembly)); + return proxy; + } + + private static AssemblyLoadContext CreatePluginAssemblyLoadContext(string name, string folder, AssemblyLoadContext? shared = null) + { + return new PluginLoadContext(name, folder, shared); + } + + private static object? CreateProxy(Assembly assembly1) + { + var proxycreator = assembly1.DefinedTypes.FirstOrDefault(t => t.Name == "ProxyFactory"); + Assert.NotNull(proxycreator); + var method = proxycreator!.GetMethod("Create", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + return method.Invoke(null, new object[] { }); + } + + private class PluginLoadContext : AssemblyLoadContext + { + private readonly AssemblyLoadContext? _sharedContext; + private readonly string _folder; + + public PluginLoadContext(string name, string folder, AssemblyLoadContext? sharedContext = null) + : base(name, isCollectible: true) + { + _sharedContext = sharedContext; + _folder = folder; + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (_sharedContext is not null) + { + return _sharedContext.LoadFromAssemblyName(assemblyName); + } + + string path = Path.Combine(_folder, assemblyName.Name + ".dll"); + if (File.Exists(path)) + { + return LoadFromAssemblyPath(path); + } + return null; + } + } +} +#endif diff --git a/tests/protobuf-net.Grpc.Test/ClientFactoryTests.cs b/tests/protobuf-net.Grpc.Test/ClientFactoryTests.cs index 1c10f1c..c75de9d 100644 --- a/tests/protobuf-net.Grpc.Test/ClientFactoryTests.cs +++ b/tests/protobuf-net.Grpc.Test/ClientFactoryTests.cs @@ -8,6 +8,7 @@ using System.ServiceModel; using System.Threading.Tasks; using Xunit; + namespace protobuf_net.Grpc.Test { public class ClientFactoryTests @@ -21,7 +22,7 @@ public void CanConfigureCodeFirstClient() o.Address = new Uri("http://localhost"); }); var serviceProvider = services.BuildServiceProvider(); - + var clientFactory = serviceProvider.GetRequiredService(); var client = clientFactory.CreateClient(nameof(IMyService)); @@ -47,16 +48,16 @@ public void CanConfigureHttpClientBuilder() var name = client.GetType().FullName; Assert.StartsWith("ProtoBuf.Grpc.Internal.Proxies.ClientBase.", name); } - } - [ServiceContract] - public interface IMyService - { - [OperationContract] - public ValueTask UnaryCall(Dummy value); - } + [ServiceContract] + public interface IMyService + { + [OperationContract] + public ValueTask UnaryCall(Dummy value); + } - [DataContract] - public class Dummy { } + [DataContract] + public class Dummy { } + } } #endif \ No newline at end of file diff --git a/tests/protobuf-net.Grpc.Test/protobuf-net.Grpc.Test.csproj b/tests/protobuf-net.Grpc.Test/protobuf-net.Grpc.Test.csproj index b178cf0..a4d05c8 100644 --- a/tests/protobuf-net.Grpc.Test/protobuf-net.Grpc.Test.csproj +++ b/tests/protobuf-net.Grpc.Test/protobuf-net.Grpc.Test.csproj @@ -1,31 +1,34 @@  - - false - net462;net472;net6.0;net8.0 - protobuf_net.Grpc.Test - false - $(NoWarn);CA1822;CS8981 - - - $(DefineConstants);CLIENT_FACTORY - + + false + net462;net472;net6.0;net8.0 + protobuf_net.Grpc.Test + false + $(NoWarn);CA1822;CS8981 + + + $(DefineConstants);CLIENT_FACTORY + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - - - - + + + + + runtime + +