From a824148681607e2b12d61ccbd157c0d53a3882fc Mon Sep 17 00:00:00 2001 From: Bill Menees Date: Wed, 14 Sep 2022 16:02:09 -0500 Subject: [PATCH] Support interface "inheritance" --- src/Directory.Build.props | 2 +- src/Menees.Remoting/RmiServer.cs | 37 +++++++++++- tests/Menees.Remoting.Tests/BaseTests.cs | 2 +- tests/Menees.Remoting.Tests/RmiServerTests.cs | 56 +++++++++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index efe0e87..008300f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -29,7 +29,7 @@ - 0.6.0-beta + 0.7.0-beta diff --git a/src/Menees.Remoting/RmiServer.cs b/src/Menees.Remoting/RmiServer.cs index 67773a2..109241e 100644 --- a/src/Menees.Remoting/RmiServer.cs +++ b/src/Menees.Remoting/RmiServer.cs @@ -19,8 +19,7 @@ public sealed class RmiServer : RmiNode, I { #region Private Data Members - private static readonly Dictionary MethodSignatureCache = - typeof(TServiceInterface).GetMethods().ToDictionary(method => GetMethodSignature(method)); + private static readonly Lazy> MethodSignatureCache = new(CreateMethodSignatureDictionary); private readonly PipeServer pipe; private readonly CancellationToken cancellationToken; @@ -134,6 +133,38 @@ protected override void Dispose(bool disposing) #region Private Methods + private static Dictionary CreateMethodSignatureDictionary() + { + List<(string Signature, MethodInfo Method)> items = new(); + + void AddMethods(Type interfaceType) + { + items.AddRange(interfaceType.GetMethods().Select(method => (GetMethodSignature(method), method))); + + foreach (Type implementedType in interfaceType.GetInterfaces()) + { + AddMethods(implementedType); + } + } + + // Calling GetMethods() on an interface type only gets its declared methods. Even BindingFlags don't help + // because a "derived" interface still has "object" as its base and any "inherited" interface is really just an + // "implements" relationship and not an "inherits" relationship. So, we have to recursively find all "inherited" + // interfaces and flatten them out ourselves. + // https://stackoverflow.com/questions/3395174/c-sharp-interface-inheritance/3395327#3395327 + AddMethods(typeof(TServiceInterface)); + + // Take the first MethodInfo instance for each signature because interfaces can have a diamond shaped + // "inheritance" pattern. For example, suppose an interface "inherits" from two interfaces that are + // both IDisposable like this: + // IStreamReader : IDisposable + // IStreamWriter : IDisposable + // IStreamReaderWriter : IStreamReader, IStreamWriter + Dictionary result = items.GroupBy(pair => pair.Signature) + .ToDictionary(pair => pair.Key, pair => pair.First().Method); + return result; + } + private async Task ProcessRequestAsync(Stream clientStream) { await ServerUtility.ProcessRequestAsync( @@ -144,7 +175,7 @@ await ServerUtility.ProcessRequestAsync( { Response response; - if (!MethodSignatureCache.TryGetValue(request.MethodSignature ?? string.Empty, out MethodInfo? method)) + if (!MethodSignatureCache.Value.TryGetValue(request.MethodSignature ?? string.Empty, out MethodInfo? method)) { response = ServerUtility.CreateResponse(new TargetException( $"A {typeof(TServiceInterface).FullName} method with signature '{request.MethodSignature}' was not found.")); diff --git a/tests/Menees.Remoting.Tests/BaseTests.cs b/tests/Menees.Remoting.Tests/BaseTests.cs index 3377d33..db8fc08 100644 --- a/tests/Menees.Remoting.Tests/BaseTests.cs +++ b/tests/Menees.Remoting.Tests/BaseTests.cs @@ -122,7 +122,7 @@ protected async Task TestCrossProcessServerAsync( } finally { - TimeSpan exitWait = TimeSpan.FromSeconds(5); + TimeSpan exitWait = TimeSpan.FromSeconds(10); WaitForExit(hostProcess, exitWait, ExpectedExitCode); } } diff --git a/tests/Menees.Remoting.Tests/RmiServerTests.cs b/tests/Menees.Remoting.Tests/RmiServerTests.cs index 23ce6f7..71fc4d7 100644 --- a/tests/Menees.Remoting.Tests/RmiServerTests.cs +++ b/tests/Menees.Remoting.Tests/RmiServerTests.cs @@ -14,6 +14,30 @@ [TestClass] public class RmiServerTests : BaseTests { + #region Private Interfaces + + private interface IRoot + { + int GetRoot(int input); + } + + private interface ILevel1A : IRoot + { + int GetLevel1A(int input); + } + + private interface ILevel1B : IRoot + { + int GetLevel1B(int input); + } + + private interface IDiamond : ILevel1A, ILevel1B + { + int GetDiamond(int input); + } + + #endregion + #region Public Methods [TestMethod] @@ -143,6 +167,23 @@ public void SecurityVariations() } } + [TestMethod] + public void InterfaceInheritance() + { + string serverPath = this.GenerateServerPath(); + Diamond diamond = new(); + using RmiServer server = new(diamond, serverPath, loggerFactory: this.LoggerFactory); + server.ReportUnhandledException = WriteUnhandledServerException; + server.Start(); + + using RmiClient client = new(serverPath, loggerFactory: this.LoggerFactory); + IDiamond proxy = client.CreateProxy(); + proxy.GetRoot(10).ShouldBe(10); + proxy.GetLevel1A(10).ShouldBe(110); + proxy.GetLevel1B(10).ShouldBe(210); + proxy.GetDiamond(10).ShouldBe(1010); + } + #endregion #region Private Methods @@ -206,4 +247,19 @@ private void TestClient(int clientCount, string serverPath, ClientSecurity? clie } #endregion + + #region Private Types + + private sealed class Diamond : IDiamond + { + public int GetDiamond(int input) => input + 1000; + + public int GetLevel1A(int input) => input + 100; + + public int GetLevel1B(int input) => input + 200; + + public int GetRoot(int input) => input; + } + + #endregion }