From 6184ac703516fbde310818b1cc88e53c53ff9be4 Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sun, 30 Jan 2022 12:44:57 +0100 Subject: [PATCH 1/6] update version to 1.4.3 --- Sources/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Versions.props b/Sources/Versions.props index 5fb204d6..2f8ba4ac 100644 --- a/Sources/Versions.props +++ b/Sources/Versions.props @@ -1,6 +1,6 @@ - 1.4.2 + 1.4.3 2.43.0 2.42.0 From b30ebf09e1ca5009799830f03041637dd8ead773 Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sun, 30 Jan 2022 13:14:17 +0100 Subject: [PATCH 2/6] shared interface --- .../SharedContractTest.cs | 53 +++++++ .../SelfHost/SharedContractTest.cs | 71 +++++++++ .../Internal/InterfaceTreeTest.Domain.cs | 135 ++++++++++++++++++ .../Generator/Internal/InterfaceTreeTest.cs | 132 +++++++++++++++++ .../Generator/Internal/ContractDescription.cs | 59 ++++---- .../Generator/Internal/InterfaceTree.cs | 113 +++++++++++++++ .../Generator/Internal/SyntaxTools.cs | 18 +++ .../SharedContractTest.cs | 50 +++++++ .../Internal/InterfaceTreeTest.Domain.cs | 135 ++++++++++++++++++ .../Internal/InterfaceTreeTest.cs | 117 +++++++++++++++ .../Domain/ConcreteContract1.cs | 30 ++++ .../Domain/ConcreteContract2.cs | 30 ++++ .../Domain/ISharedContract.cs | 43 ++++++ .../SharedContractTestBase.cs | 50 +++++++ .../Internal/ContractDescription.cs | 58 ++++---- .../Internal/InterfaceTree.cs | 114 +++++++++++++++ 16 files changed, 1157 insertions(+), 51 deletions(-) create mode 100644 Sources/ServiceModel.Grpc.AspNetCore.Test/SharedContractTest.cs create mode 100644 Sources/ServiceModel.Grpc.DesignTime.Generator.Test/SelfHost/SharedContractTest.cs create mode 100644 Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs create mode 100644 Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs create mode 100644 Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/InterfaceTree.cs create mode 100644 Sources/ServiceModel.Grpc.SelfHost.Test/SharedContractTest.cs create mode 100644 Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.Domain.cs create mode 100644 Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.cs create mode 100644 Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract1.cs create mode 100644 Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract2.cs create mode 100644 Sources/ServiceModel.Grpc.TestApi/Domain/ISharedContract.cs create mode 100644 Sources/ServiceModel.Grpc.TestApi/SharedContractTestBase.cs create mode 100644 Sources/ServiceModel.Grpc/Internal/InterfaceTree.cs diff --git a/Sources/ServiceModel.Grpc.AspNetCore.Test/SharedContractTest.cs b/Sources/ServiceModel.Grpc.AspNetCore.Test/SharedContractTest.cs new file mode 100644 index 00000000..b8b4e44d --- /dev/null +++ b/Sources/ServiceModel.Grpc.AspNetCore.Test/SharedContractTest.cs @@ -0,0 +1,53 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using NUnit.Framework; +using ServiceModel.Grpc.AspNetCore.TestApi; +using ServiceModel.Grpc.TestApi; +using ServiceModel.Grpc.TestApi.Domain; + +namespace ServiceModel.Grpc.AspNetCore +{ + [TestFixture] + public class SharedContractTest : SharedContractTestBase + { + private KestrelHost _host = null!; + + [OneTimeSetUp] + public async Task BeforeAll() + { + _host = await new KestrelHost() + .ConfigureEndpoints(endpoints => + { + endpoints.MapGrpcService(); + endpoints.MapGrpcService(); + }) + .StartAsync() + .ConfigureAwait(false); + + DomainService1 = _host.ClientFactory.CreateClient(_host.Channel); + DomainService2 = _host.ClientFactory.CreateClient(_host.Channel); + } + + [OneTimeTearDown] + public async Task AfterAll() + { + await _host.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/Sources/ServiceModel.Grpc.DesignTime.Generator.Test/SelfHost/SharedContractTest.cs b/Sources/ServiceModel.Grpc.DesignTime.Generator.Test/SelfHost/SharedContractTest.cs new file mode 100644 index 00000000..ff3f35b5 --- /dev/null +++ b/Sources/ServiceModel.Grpc.DesignTime.Generator.Test/SelfHost/SharedContractTest.cs @@ -0,0 +1,71 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Grpc.Core; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using ServiceModel.Grpc.Client; +using ServiceModel.Grpc.TestApi; +using ServiceModel.Grpc.TestApi.Domain; +using GrpcChannel = Grpc.Core.Channel; + +namespace ServiceModel.Grpc.DesignTime.Generator.Test.SelfHost +{ + [TestFixture] + [ExportGrpcService(typeof(IConcreteContract1), GenerateSelfHostExtensions = true)] + [ImportGrpcService(typeof(IConcreteContract1))] + [ExportGrpcService(typeof(ConcreteContract2), GenerateSelfHostExtensions = true)] + [ImportGrpcService(typeof(IConcreteContract2))] + public partial class SharedContractTest : SharedContractTestBase + { + private const int Port = 8080; + private Server _server = null!; + private GrpcChannel _channel = null!; + + [OneTimeSetUp] + public void BeforeAll() + { + var provider = new ServiceCollection().AddTransient().BuildServiceProvider(); + + _server = new Server + { + Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) } + }; + + _channel = new GrpcChannel("localhost", Port, ChannelCredentials.Insecure); + + AddConcreteContract1(_server.Services, provider); + AddConcreteContract2(_server.Services, new ConcreteContract2()); + + var clientFactory = new ClientFactory(); + AddConcreteContract1Client(clientFactory); + AddConcreteContract2Client(clientFactory); + + DomainService1 = clientFactory.CreateClient(_channel); + DomainService2 = clientFactory.CreateClient(_channel); + + _server.Start(); + } + + [OneTimeTearDown] + public async Task AfterAll() + { + await _channel.ShutdownAsync().ConfigureAwait(false); + await _server.ShutdownAsync().ConfigureAwait(false); + } + } +} diff --git a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs new file mode 100644 index 00000000..7f67dc42 --- /dev/null +++ b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs @@ -0,0 +1,135 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.ServiceModel; + +//// ReSharper disable OperationContractWithoutServiceContract + +namespace ServiceModel.Grpc.DesignTime.Generator.Internal +{ + public partial class InterfaceTreeTest + { + public static class NonServiceContract + { + public interface IService + { + [OperationContract] + void Method(); + } + } + + public static class OneContractRoot + { + public interface IService1 : IDisposable + { + [OperationContract] + void Method1(); + } + + public interface IService2 + { + [OperationContract] + void Method2(); + } + + [ServiceContract] + public interface IContract : IService1, IService2 + { + } + + public sealed class Contract : IContract + { + public void Method1() => throw new NotImplementedException(); + + public void Method2() => throw new NotImplementedException(); + + public void Dispose() => throw new NotImplementedException(); + } + } + + // attach non ServiceContract to the most top: IService1 must be attached to IContract2 + // other behavior does not make sense, the following must work: + // - call IService1 via IContract2 + // - call IService1 via IContract1 + public static class AttachToMostTopContract + { + public interface IService1 + { + [OperationContract] + void Method1(); + } + + public interface IService2 + { + [OperationContract] + void Method2(); + } + + [ServiceContract] + public interface IContract1 : IService1, IService2 + { + } + + [ServiceContract] + public interface IContract2 : IContract1 + { + } + } + + public static class RootNotFound + { + public interface IService + { + [OperationContract] + void Method(); + } + + [ServiceContract] + public interface IContract1 : IService + { + } + + [ServiceContract] + public interface IContract2 : IService + { + } + + public sealed class Contract : IContract1, IContract2 + { + public void Method() => throw new NotImplementedException(); + } + } + + public static class TransientInterface + { + public interface IService1 + { + [OperationContract] + void Method(); + } + + public interface IService2 : IService1 + { + } + + [ServiceContract] + public interface IContract : IService2 + { + } + } + } +} diff --git a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs new file mode 100644 index 00000000..c6458f11 --- /dev/null +++ b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs @@ -0,0 +1,132 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NUnit.Framework; +using Shouldly; + +namespace ServiceModel.Grpc.DesignTime.Generator.Internal +{ + [TestFixture] + public partial class InterfaceTreeTest + { + private static readonly Compilation Compilation = CSharpCompilation + .Create( + nameof(InterfaceTreeTest), + references: new[] + { + MetadataReference.CreateFromFile(typeof(InterfaceTreeTest).Assembly.Location) + }); + + [Test] + public void NonServiceContractTree() + { + var rootType = Compilation.GetTypeByMetadataName(typeof(NonServiceContract.IService)); + var sut = new InterfaceTree(rootType); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].ShouldBe(rootType); + + sut.Services.ShouldBeEmpty(); + } + + [Test] + [TestCase(typeof(OneContractRoot.IContract))] + [TestCase(typeof(OneContractRoot.Contract))] + public void OneContractRootTree(Type rootType) + { + var rootTypeSymbol = Compilation.GetTypeByMetadataName(rootType); + var sut = new InterfaceTree(rootTypeSymbol); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].Name.ShouldBe(nameof(IDisposable)); + + sut.Services.Count.ShouldBe(3); + + sut.Services[0].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[0].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(OneContractRoot.IContract))); + + sut.Services[1].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(OneContractRoot.IService1))); + + sut.Services[2].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[2].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(OneContractRoot.IService2))); + } + + [Test] + public void AttachToMostTopContractTree() + { + var rootType = Compilation.GetTypeByMetadataName(typeof(AttachToMostTopContract.IContract2)); + var sut = new InterfaceTree(rootType); + + sut.Interfaces.ShouldBeEmpty(); + + sut.Services.Count.ShouldBe(4); + + sut.Services[0].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[0].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(AttachToMostTopContract.IContract2))); + + sut.Services[1].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract1)); + sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(AttachToMostTopContract.IContract1))); + + sut.Services[2].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[2].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(AttachToMostTopContract.IService1))); + + sut.Services[3].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[3].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(AttachToMostTopContract.IService2))); + } + + [Test] + public void RootNotFoundTree() + { + var rootType = Compilation.GetTypeByMetadataName(typeof(RootNotFound.Contract)); + var sut = new InterfaceTree(rootType); + + sut.Interfaces.ShouldBeEmpty(); + + sut.Services.Count.ShouldBe(3); + + sut.Services[0].ServiceName.ShouldBe(nameof(RootNotFound.IContract1)); + sut.Services[0].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(RootNotFound.IContract1))); + + sut.Services[1].ServiceName.ShouldBe(nameof(RootNotFound.IContract2)); + sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(RootNotFound.IContract2))); + + sut.Services[2].ServiceName.ShouldBe(nameof(RootNotFound.IContract1)); + sut.Services[2].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(RootNotFound.IService))); + } + + [Test] + public void TransientInterfaceTree() + { + var rootType = Compilation.GetTypeByMetadataName(typeof(TransientInterface.IContract)); + var sut = new InterfaceTree(rootType); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientInterface.IService2))); + + sut.Services.Count.ShouldBe(2); + + sut.Services[0].ServiceName.ShouldBe(nameof(TransientInterface.IContract)); + sut.Services[0].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientInterface.IContract))); + + sut.Services[1].ServiceName.ShouldBe(nameof(TransientInterface.IContract)); + sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientInterface.IService1))); + } + } +} diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/ContractDescription.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/ContractDescription.cs index 18962b8b..c02c2139 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/ContractDescription.cs +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/ContractDescription.cs @@ -147,39 +147,35 @@ private static bool TryCreateOperation( return null; } + private static NotSupportedMethodDescription CreateNonServiceOperation(INamedTypeSymbol interfaceType, IMethodSymbol method) + { + var error = "Method {0}.{1}.{2} is not service operation.".FormatWith( + SyntaxTools.GetNamespace(interfaceType), + interfaceType.Name, + method.Name); + + return new NotSupportedMethodDescription(method, error); + } + private void AnalyzeServiceAndInterfaces(INamedTypeSymbol serviceType) { - foreach (var interfaceType in SyntaxTools.ExpandInterface(serviceType)) - { - var interfaceDescription = new InterfaceDescription(interfaceType); + var tree = new InterfaceTree(serviceType); - string? serviceName = null; - if (ServiceContract.IsServiceContractInterface(interfaceType)) - { - serviceName = ServiceContract.GetServiceName(interfaceType); - Services.Add(interfaceDescription); - } - else - { - Interfaces.Add(interfaceDescription); - } + for (var i = 0; i < tree.Services.Count; i++) + { + var service = tree.Services[i]; + var interfaceDescription = new InterfaceDescription(service.ServiceType); + Services.Add(interfaceDescription); - foreach (var method in SyntaxTools.GetInstanceMethods(interfaceType)) + foreach (var method in SyntaxTools.GetInstanceMethods(service.ServiceType)) { - string? error; - - if (serviceName == null || !ServiceContract.IsServiceOperation(method)) + if (!ServiceContract.IsServiceOperation(method)) { - error = "Method {0}.{1}.{2} is not service operation.".FormatWith( - SyntaxTools.GetNamespace(interfaceType), - interfaceType.Name, - method.Name); - - interfaceDescription.Methods.Add(new NotSupportedMethodDescription(method, error)); + interfaceDescription.Methods.Add(CreateNonServiceOperation(service.ServiceType, method)); continue; } - if (TryCreateOperation(method, serviceName, ServiceContract.GetServiceOperationName(method), out var operation, out error)) + if (TryCreateOperation(method, service.ServiceName, ServiceContract.GetServiceOperationName(method), out var operation, out var error)) { interfaceDescription.Operations.Add(operation); } @@ -189,6 +185,18 @@ private void AnalyzeServiceAndInterfaces(INamedTypeSymbol serviceType) } } } + + for (var i = 0; i < tree.Interfaces.Count; i++) + { + var interfaceType = tree.Interfaces[i]; + var interfaceDescription = new InterfaceDescription(interfaceType); + Interfaces.Add(interfaceDescription); + + foreach (var method in SyntaxTools.GetInstanceMethods(interfaceType)) + { + interfaceDescription.Methods.Add(CreateNonServiceOperation(interfaceType, method)); + } + } } private void SortAll() @@ -244,12 +252,11 @@ private void FindSyncOverAsync() for (var i = 0; i < Services.Count; i++) { var service = Services[i]; - var serviceName = ServiceContract.GetServiceName(service.InterfaceType); for (var j = 0; j < service.Methods.Count; j++) { var syncMethod = service.Methods[j]; - if (!TryCreateOperation(syncMethod.Method.Source, serviceName, "dummy", out var syncOperation, out _) + if (!TryCreateOperation(syncMethod.Method.Source, "dummy", "dummy", out var syncOperation, out _) || syncOperation.OperationType != MethodType.Unary || syncOperation.IsAsync) { diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/InterfaceTree.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/InterfaceTree.cs new file mode 100644 index 00000000..ccfc44ed --- /dev/null +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/InterfaceTree.cs @@ -0,0 +1,113 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace ServiceModel.Grpc.DesignTime.Generator.Internal +{ + internal readonly ref struct InterfaceTree + { + public InterfaceTree(INamedTypeSymbol rootType) + { + Services = new List<(string ServiceName, INamedTypeSymbol ServiceType)>(); + Interfaces = new List(); + + var interfaces = SyntaxTools.ExpandInterface(rootType).ToList(); + ExtractServiceContracts(interfaces); + ExtractAttachedContracts(interfaces); + Interfaces.AddRange(interfaces); + } + + public List<(string ServiceName, INamedTypeSymbol ServiceType)> Services { get; } + + public List Interfaces { get; } + + private static bool ContainsOperation(INamedTypeSymbol type) + { + foreach (var method in SyntaxTools.GetInstanceMethods(type)) + { + if (ServiceContract.IsServiceOperation(method)) + { + return true; + } + } + + return false; + } + + private void ExtractServiceContracts(List interfaces) + { + for (var i = 0; i < interfaces.Count; i++) + { + var interfaceType = interfaces[i]; + if (!ServiceContract.IsServiceContractInterface(interfaceType)) + { + continue; + } + + var serviceName = ServiceContract.GetServiceName(interfaceType); + Services.Add((serviceName, interfaceType)); + interfaces.RemoveAt(i); + i--; + } + } + + private void ExtractAttachedContracts(List interfaces) + { + // take into account only ServiceContracts + var servicesIndex = Services.Count; + + for (var i = 0; i < interfaces.Count; i++) + { + var interfaceType = interfaces[i]; + if (!ContainsOperation(interfaceType) || !TryFindParentService(interfaceType, servicesIndex, out var serviceName)) + { + continue; + } + + Services.Add((serviceName, interfaceType)); + interfaces.RemoveAt(i); + i--; + } + } + + private bool TryFindParentService(INamedTypeSymbol interfaceType, int servicesIndex, [NotNullWhen(true)] out string? serviceName) + { + serviceName = null; + INamedTypeSymbol? parent = null; + + for (var i = 0; i < servicesIndex; i++) + { + var test = Services[i]; + if (!interfaceType.IsAssignableFrom(test.ServiceType)) + { + continue; + } + + if (parent == null || parent.IsAssignableFrom(test.ServiceType)) + { + parent = test.ServiceType; + serviceName = test.ServiceName; + } + } + + return parent != null; + } + } +} diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs index 1ec25529..5d95a948 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs @@ -203,6 +203,24 @@ public static bool IsAssignableFrom(this ITypeSymbol type, Type expected) return false; } + public static bool IsAssignableFrom(this ITypeSymbol type, ITypeSymbol expected) + { + if (SymbolEqualityComparer.Default.Equals(type, expected)) + { + return true; + } + + foreach (var i in expected.Interfaces) + { + if (SymbolEqualityComparer.Default.Equals(i, type)) + { + return true; + } + } + + return false; + } + public static bool IsVoid(ITypeSymbol type) => IsMatch(type, typeof(void)); public static bool IsTask(ITypeSymbol type) diff --git a/Sources/ServiceModel.Grpc.SelfHost.Test/SharedContractTest.cs b/Sources/ServiceModel.Grpc.SelfHost.Test/SharedContractTest.cs new file mode 100644 index 00000000..64a5c3b2 --- /dev/null +++ b/Sources/ServiceModel.Grpc.SelfHost.Test/SharedContractTest.cs @@ -0,0 +1,50 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Grpc.Core; +using NUnit.Framework; +using ServiceModel.Grpc.Client; +using ServiceModel.Grpc.TestApi; +using ServiceModel.Grpc.TestApi.Domain; + +namespace ServiceModel.Grpc.SelfHost +{ + [TestFixture] + public class SharedContractTest : SharedContractTestBase + { + private ServerHost _host = null!; + + [OneTimeSetUp] + public void BeforeAll() + { + _host = new ServerHost(); + + _host.Services.AddServiceModelSingleton(new ConcreteContract1()); + _host.Services.AddServiceModelSingleton(new ConcreteContract2()); + DomainService1 = new ClientFactory().CreateClient(_host.Channel); + DomainService2 = new ClientFactory().CreateClient(_host.Channel); + + _host.Start(); + } + + [OneTimeTearDown] + public async Task AfterAll() + { + await _host.DisposeAsync().ConfigureAwait(false); + } + } +} diff --git a/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.Domain.cs b/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.Domain.cs new file mode 100644 index 00000000..bc559171 --- /dev/null +++ b/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.Domain.cs @@ -0,0 +1,135 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.ServiceModel; + +//// ReSharper disable OperationContractWithoutServiceContract + +namespace ServiceModel.Grpc.Internal +{ + public partial class InterfaceTreeTest + { + public static class NonServiceContract + { + public interface IService + { + [OperationContract] + void Method(); + } + } + + public static class OneContractRoot + { + public interface IService1 : IDisposable + { + [OperationContract] + void Method1(); + } + + public interface IService2 + { + [OperationContract] + void Method2(); + } + + [ServiceContract] + public interface IContract : IService1, IService2 + { + } + + public sealed class Contract : IContract + { + public void Method1() => throw new NotImplementedException(); + + public void Method2() => throw new NotImplementedException(); + + public void Dispose() => throw new NotImplementedException(); + } + } + + // attach non ServiceContract to the most top: IService1 must be attached to IContract2 + // other behavior does not make sense, the following must work: + // - call IService1 via IContract2 + // - call IService1 via IContract1 + public static class AttachToMostTopContract + { + public interface IService1 + { + [OperationContract] + void Method1(); + } + + public interface IService2 + { + [OperationContract] + void Method2(); + } + + [ServiceContract] + public interface IContract1 : IService1, IService2 + { + } + + [ServiceContract] + public interface IContract2 : IContract1 + { + } + } + + public static class RootNotFound + { + public interface IService + { + [OperationContract] + void Method(); + } + + [ServiceContract] + public interface IContract1 : IService + { + } + + [ServiceContract] + public interface IContract2 : IService + { + } + + public sealed class Contract : IContract1, IContract2 + { + public void Method() => throw new NotImplementedException(); + } + } + + public static class TransientInterface + { + public interface IService1 + { + [OperationContract] + void Method(); + } + + public interface IService2 : IService1 + { + } + + [ServiceContract] + public interface IContract : IService2 + { + } + } + } +} diff --git a/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.cs b/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.cs new file mode 100644 index 00000000..37ba5439 --- /dev/null +++ b/Sources/ServiceModel.Grpc.Test/Internal/InterfaceTreeTest.cs @@ -0,0 +1,117 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using NUnit.Framework; +using Shouldly; + +namespace ServiceModel.Grpc.Internal +{ + [TestFixture] + public partial class InterfaceTreeTest + { + [Test] + public void NonServiceContractTree() + { + var sut = new InterfaceTree(typeof(NonServiceContract.IService)); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].ShouldBe(typeof(NonServiceContract.IService)); + + sut.Services.ShouldBeEmpty(); + } + + [Test] + [TestCase(typeof(OneContractRoot.IContract))] + [TestCase(typeof(OneContractRoot.Contract))] + public void OneContractRootTree(Type rootType) + { + var sut = new InterfaceTree(rootType); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].ShouldBe(typeof(IDisposable)); + + sut.Services.Count.ShouldBe(3); + + sut.Services[0].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[0].ServiceType.ShouldBe(typeof(OneContractRoot.IContract)); + + sut.Services[1].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[1].ServiceType.ShouldBe(typeof(OneContractRoot.IService1)); + + sut.Services[2].ServiceName.ShouldBe(nameof(OneContractRoot.IContract)); + sut.Services[2].ServiceType.ShouldBe(typeof(OneContractRoot.IService2)); + } + + [Test] + public void AttachToMostTopContractTree() + { + var sut = new InterfaceTree(typeof(AttachToMostTopContract.IContract2)); + + sut.Interfaces.ShouldBeEmpty(); + + sut.Services.Count.ShouldBe(4); + + sut.Services[0].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[0].ServiceType.ShouldBe(typeof(AttachToMostTopContract.IContract2)); + + sut.Services[1].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract1)); + sut.Services[1].ServiceType.ShouldBe(typeof(AttachToMostTopContract.IContract1)); + + sut.Services[2].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[2].ServiceType.ShouldBe(typeof(AttachToMostTopContract.IService1)); + + sut.Services[3].ServiceName.ShouldBe(nameof(AttachToMostTopContract.IContract2)); + sut.Services[3].ServiceType.ShouldBe(typeof(AttachToMostTopContract.IService2)); + } + + [Test] + public void RootNotFoundTree() + { + var sut = new InterfaceTree(typeof(RootNotFound.Contract)); + + sut.Interfaces.ShouldBeEmpty(); + + sut.Services.Count.ShouldBe(3); + + sut.Services[0].ServiceName.ShouldBe(nameof(RootNotFound.IContract1)); + sut.Services[0].ServiceType.ShouldBe(typeof(RootNotFound.IContract1)); + + sut.Services[1].ServiceName.ShouldBe(nameof(RootNotFound.IContract2)); + sut.Services[1].ServiceType.ShouldBe(typeof(RootNotFound.IContract2)); + + sut.Services[2].ServiceName.ShouldBe(nameof(RootNotFound.IContract1)); + sut.Services[2].ServiceType.ShouldBe(typeof(RootNotFound.IService)); + } + + [Test] + public void TransientInterfaceTree() + { + var sut = new InterfaceTree(typeof(TransientInterface.IContract)); + + sut.Interfaces.Count.ShouldBe(1); + sut.Interfaces[0].ShouldBe(typeof(TransientInterface.IService2)); + + sut.Services.Count.ShouldBe(2); + + sut.Services[0].ServiceName.ShouldBe(nameof(TransientInterface.IContract)); + sut.Services[0].ServiceType.ShouldBe(typeof(TransientInterface.IContract)); + + sut.Services[1].ServiceName.ShouldBe(nameof(TransientInterface.IContract)); + sut.Services[1].ServiceType.ShouldBe(typeof(TransientInterface.IService1)); + } + } +} diff --git a/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract1.cs b/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract1.cs new file mode 100644 index 00000000..dc22c4f7 --- /dev/null +++ b/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract1.cs @@ -0,0 +1,30 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; + +namespace ServiceModel.Grpc.TestApi.Domain +{ + public sealed class ConcreteContract1 : IConcreteContract1 + { + public Task GetName() => GetConcreteName(); + + public Task GetConcreteName() + { + return Task.FromResult(GetType().Name); + } + } +} \ No newline at end of file diff --git a/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract2.cs b/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract2.cs new file mode 100644 index 00000000..6ff02c41 --- /dev/null +++ b/Sources/ServiceModel.Grpc.TestApi/Domain/ConcreteContract2.cs @@ -0,0 +1,30 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; + +namespace ServiceModel.Grpc.TestApi.Domain +{ + public sealed class ConcreteContract2 : IConcreteContract2 + { + public Task GetName() => GetConcreteName(); + + public Task GetConcreteName() + { + return Task.FromResult(GetType().Name); + } + } +} \ No newline at end of file diff --git a/Sources/ServiceModel.Grpc.TestApi/Domain/ISharedContract.cs b/Sources/ServiceModel.Grpc.TestApi/Domain/ISharedContract.cs new file mode 100644 index 00000000..7fc77f12 --- /dev/null +++ b/Sources/ServiceModel.Grpc.TestApi/Domain/ISharedContract.cs @@ -0,0 +1,43 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.ServiceModel; +using System.Threading.Tasks; + +//// ReSharper disable OperationContractWithoutServiceContract + +namespace ServiceModel.Grpc.TestApi.Domain +{ + public interface ISharedContract + { + [OperationContract] + Task GetName(); + } + + [ServiceContract] + public interface IConcreteContract1 : ISharedContract + { + [OperationContract] + Task GetConcreteName(); + } + + [ServiceContract] + public interface IConcreteContract2 : ISharedContract + { + [OperationContract] + Task GetConcreteName(); + } +} diff --git a/Sources/ServiceModel.Grpc.TestApi/SharedContractTestBase.cs b/Sources/ServiceModel.Grpc.TestApi/SharedContractTestBase.cs new file mode 100644 index 00000000..aafc0714 --- /dev/null +++ b/Sources/ServiceModel.Grpc.TestApi/SharedContractTestBase.cs @@ -0,0 +1,50 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using NUnit.Framework; +using ServiceModel.Grpc.TestApi.Domain; +using Shouldly; + +namespace ServiceModel.Grpc.TestApi +{ + public abstract class SharedContractTestBase + { + protected IConcreteContract1 DomainService1 { get; set; } = null!; + + protected IConcreteContract2 DomainService2 { get; set; } = null!; + + [Test] + public async Task InvokeConcreteContract1() + { + var name1 = await DomainService1.GetName().ConfigureAwait(false); + var name2 = await DomainService1.GetConcreteName().ConfigureAwait(false); + + name1.ShouldBe(nameof(ConcreteContract1)); + name1.ShouldBe(name2); + } + + [Test] + public async Task InvokeConcreteContract2() + { + var name1 = await DomainService2.GetName().ConfigureAwait(false); + var name2 = await DomainService2.GetConcreteName().ConfigureAwait(false); + + name1.ShouldBe(nameof(ConcreteContract2)); + name1.ShouldBe(name2); + } + } +} diff --git a/Sources/ServiceModel.Grpc/Internal/ContractDescription.cs b/Sources/ServiceModel.Grpc/Internal/ContractDescription.cs index c1de9c35..aebe93a7 100644 --- a/Sources/ServiceModel.Grpc/Internal/ContractDescription.cs +++ b/Sources/ServiceModel.Grpc/Internal/ContractDescription.cs @@ -132,42 +132,38 @@ private static bool TryCreateMessage(MethodInfo method, [MaybeNullWhen(false)] o return null; } + private static MethodDescription CreateNonServiceOperation(Type interfaceType, MethodInfo method) + { + var error = "Method {0}.{1}.{2} is not service operation.".FormatWith( + ReflectionTools.GetNamespace(interfaceType), + interfaceType.Name, + method.Name); + + return new MethodDescription(method, error); + } + private void AnalyzeServiceAndInterfaces(Type serviceType) { - foreach (var interfaceType in ReflectionTools.ExpandInterface(serviceType)) - { - var interfaceDescription = new InterfaceDescription(interfaceType); + var tree = new InterfaceTree(serviceType); - string? serviceName = null; - if (ServiceContract.IsServiceContractInterface(interfaceType)) - { - serviceName = ServiceContract.GetServiceName(interfaceType); - Services.Add(interfaceDescription); - } - else - { - Interfaces.Add(interfaceDescription); - } + for (var i = 0; i < tree.Services.Count; i++) + { + var service = tree.Services[i]; + var interfaceDescription = new InterfaceDescription(service.ServiceType); + Services.Add(interfaceDescription); - foreach (var method in ReflectionTools.GetMethods(interfaceType)) + foreach (var method in ReflectionTools.GetMethods(service.ServiceType)) { - string? error; - - if (serviceName == null || !ServiceContract.IsServiceOperation(method)) + if (!ServiceContract.IsServiceOperation(method)) { - error = "Method {0}.{1}.{2} is not service operation.".FormatWith( - ReflectionTools.GetNamespace(interfaceType), - interfaceType.Name, - method.Name); - - interfaceDescription.Methods.Add(new MethodDescription(method, error)); + interfaceDescription.Methods.Add(CreateNonServiceOperation(service.ServiceType, method)); continue; } - if (TryCreateMessage(method, out var message, out error)) + if (TryCreateMessage(method, out var message, out var error)) { interfaceDescription.Operations.Add(new OperationDescription( - serviceName, + service.ServiceName, ServiceContract.GetServiceOperationName(method), message)); } @@ -177,6 +173,18 @@ private void AnalyzeServiceAndInterfaces(Type serviceType) } } } + + for (var i = 0; i < tree.Interfaces.Count; i++) + { + var interfaceType = tree.Interfaces[i]; + var interfaceDescription = new InterfaceDescription(interfaceType); + Interfaces.Add(interfaceDescription); + + foreach (var method in ReflectionTools.GetMethods(interfaceType)) + { + interfaceDescription.Methods.Add(CreateNonServiceOperation(interfaceType, method)); + } + } } private void FindDuplicates() diff --git a/Sources/ServiceModel.Grpc/Internal/InterfaceTree.cs b/Sources/ServiceModel.Grpc/Internal/InterfaceTree.cs new file mode 100644 index 00000000..04baf6b5 --- /dev/null +++ b/Sources/ServiceModel.Grpc/Internal/InterfaceTree.cs @@ -0,0 +1,114 @@ +// +// Copyright 2022 Max Ieremenko +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace ServiceModel.Grpc.Internal +{ + internal readonly ref struct InterfaceTree + { + public InterfaceTree(Type rootType) + { + Services = new List<(string ServiceName, Type ServiceType)>(); + Interfaces = new List(); + + var interfaces = ReflectionTools.ExpandInterface(rootType).ToList(); + ExtractServiceContracts(interfaces); + ExtractAttachedContracts(interfaces); + Interfaces.AddRange(interfaces); + } + + public List<(string ServiceName, Type ServiceType)> Services { get; } + + public List Interfaces { get; } + + private static bool ContainsOperation(Type type) + { + var methods = ReflectionTools.GetMethods(type); + for (var i = 0; i < methods.Count; i++) + { + if (ServiceContract.IsServiceOperation(methods[i])) + { + return true; + } + } + + return false; + } + + private void ExtractServiceContracts(List interfaces) + { + for (var i = 0; i < interfaces.Count; i++) + { + var interfaceType = interfaces[i]; + if (!ServiceContract.IsServiceContractInterface(interfaceType)) + { + continue; + } + + var serviceName = ServiceContract.GetServiceName(interfaceType); + Services.Add((serviceName, interfaceType)); + interfaces.RemoveAt(i); + i--; + } + } + + private void ExtractAttachedContracts(List interfaces) + { + // take into account only ServiceContracts + var servicesIndex = Services.Count; + + for (var i = 0; i < interfaces.Count; i++) + { + var interfaceType = interfaces[i]; + if (!ContainsOperation(interfaceType) || !TryFindParentService(interfaceType, servicesIndex, out var serviceName)) + { + continue; + } + + Services.Add((serviceName, interfaceType)); + interfaces.RemoveAt(i); + i--; + } + } + + private bool TryFindParentService(Type interfaceType, int servicesIndex, [NotNullWhen(true)] out string? serviceName) + { + serviceName = null; + Type? parent = null; + + for (var i = 0; i < servicesIndex; i++) + { + var test = Services[i]; + if (!interfaceType.IsAssignableFrom(test.ServiceType)) + { + continue; + } + + if (parent == null || parent.IsAssignableFrom(test.ServiceType)) + { + parent = test.ServiceType; + serviceName = test.ServiceName; + } + } + + return parent != null; + } + } +} From ddd571923b4616ae595915ec53632367df386f32 Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sun, 30 Jan 2022 15:01:18 +0100 Subject: [PATCH 3/6] example InterfaceInheritance --- .../interface-inheritance-ci-linux.ps1 | 26 ++++++ .../interface-inheritance-locally.ps1 | 27 +++++++ .../InterfaceInheritance/Client/Client.csproj | 17 ++++ .../Client/ClientCalls.cs | 80 +++++++++++++++++++ .../Client/MyGrpcProxies.cs | 10 +++ .../Contract/Contract.csproj | 12 +++ .../Contract/ICalculator{TValue}.cs | 17 ++++ .../Contract/IDoubleCalculator.cs | 12 +++ .../Contract/IGenericCalculator{TValue}.cs | 12 +++ .../Contract/IRemoteService.cs | 13 +++ .../Demo.ServerAspNetCore.csproj | 18 +++++ .../Demo.ServerAspNetCore.csproj.user | 9 +++ .../Demo.ServerAspNetCore/MyGrpcServices.cs | 10 +++ .../Demo.ServerAspNetCore/Program.cs | 51 ++++++++++++ .../Properties/launchSettings.json | 11 +++ .../Demo.ServerAspNetCore/Startup.cs | 29 +++++++ .../Demo.ServerAspNetCore/appsettings.json | 18 +++++ .../Demo.ServerSelfHost.csproj | 19 +++++ .../Demo.ServerSelfHost/MyGrpcServices.cs | 10 +++ .../Demo.ServerSelfHost/Program.cs | 44 ++++++++++ .../InterfaceInheritance.sln | 56 +++++++++++++ .../Service/DoubleCalculator.cs | 34 ++++++++ .../Service/GenericCalculator{TValue}.cs | 69 ++++++++++++++++ .../Service/Service.csproj | 11 +++ 24 files changed, 615 insertions(+) create mode 100644 Build/sdk-test/interface-inheritance-ci-linux.ps1 create mode 100644 Build/sdk-test/interface-inheritance-locally.ps1 create mode 100644 Examples/InterfaceInheritance/Client/Client.csproj create mode 100644 Examples/InterfaceInheritance/Client/ClientCalls.cs create mode 100644 Examples/InterfaceInheritance/Client/MyGrpcProxies.cs create mode 100644 Examples/InterfaceInheritance/Contract/Contract.csproj create mode 100644 Examples/InterfaceInheritance/Contract/ICalculator{TValue}.cs create mode 100644 Examples/InterfaceInheritance/Contract/IDoubleCalculator.cs create mode 100644 Examples/InterfaceInheritance/Contract/IGenericCalculator{TValue}.cs create mode 100644 Examples/InterfaceInheritance/Contract/IRemoteService.cs create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj.user create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/MyGrpcServices.cs create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/Program.cs create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/Properties/launchSettings.json create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/Startup.cs create mode 100644 Examples/InterfaceInheritance/Demo.ServerAspNetCore/appsettings.json create mode 100644 Examples/InterfaceInheritance/Demo.ServerSelfHost/Demo.ServerSelfHost.csproj create mode 100644 Examples/InterfaceInheritance/Demo.ServerSelfHost/MyGrpcServices.cs create mode 100644 Examples/InterfaceInheritance/Demo.ServerSelfHost/Program.cs create mode 100644 Examples/InterfaceInheritance/InterfaceInheritance.sln create mode 100644 Examples/InterfaceInheritance/Service/DoubleCalculator.cs create mode 100644 Examples/InterfaceInheritance/Service/GenericCalculator{TValue}.cs create mode 100644 Examples/InterfaceInheritance/Service/Service.csproj diff --git a/Build/sdk-test/interface-inheritance-ci-linux.ps1 b/Build/sdk-test/interface-inheritance-ci-linux.ps1 new file mode 100644 index 00000000..e3435a9d --- /dev/null +++ b/Build/sdk-test/interface-inheritance-ci-linux.ps1 @@ -0,0 +1,26 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + $Settings +) + +Enter-Build { + $exampleDir = Join-Path $Settings.examples "InterfaceInheritance" +} + +task Default Build, Run + +task Build { + exec { dotnet restore $exampleDir } + exec { dotnet build --configuration Release $exampleDir } +} + +task Run { + $apps = @("Demo.ServerAspNetCore", "Demo.ServerSelfHost") + foreach ($app in $apps) { + Write-Output "=== exec $app ===" + + $entryPoint = Join-Path $exampleDir "$app/bin/Release/net6.0/$app.dll" + exec { dotnet $entryPoint } + } +} diff --git a/Build/sdk-test/interface-inheritance-locally.ps1 b/Build/sdk-test/interface-inheritance-locally.ps1 new file mode 100644 index 00000000..bba6a743 --- /dev/null +++ b/Build/sdk-test/interface-inheritance-locally.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + $Settings +) + +task Default Clean, Build, Run + +task Clean { + Remove-DirectoryRecurse -Path (Join-Path $settings.examples "InterfaceInheritance") -Filters "bin", "obj" +} + +task Build { + Build-ExampleInContainer ` + -Sources $settings.sources ` + -Examples $settings.examples ` + -Packages $settings.buildOut ` + -ExampleName "InterfaceInheritance" ` + -DotNet "net6.0" +} + +task Run { + Invoke-ExampleInContainer ` + -Example (Join-Path $settings.examples "InterfaceInheritance") ` + -DotNet "net6.0" ` + -Apps "Demo.ServerAspNetCore", "Demo.ServerSelfHost" +} \ No newline at end of file diff --git a/Examples/InterfaceInheritance/Client/Client.csproj b/Examples/InterfaceInheritance/Client/Client.csproj new file mode 100644 index 00000000..fd018eaa --- /dev/null +++ b/Examples/InterfaceInheritance/Client/Client.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + + + + + + + + + + + + + diff --git a/Examples/InterfaceInheritance/Client/ClientCalls.cs b/Examples/InterfaceInheritance/Client/ClientCalls.cs new file mode 100644 index 00000000..4d4b745f --- /dev/null +++ b/Examples/InterfaceInheritance/Client/ClientCalls.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using Contract; +using Grpc.Core; +using ServiceModel.Grpc.Client; + +namespace Client +{ + public sealed class ClientCalls + { + private readonly IClientFactory _clientFactory; + private readonly Channel _channel; + + public ClientCalls(int serverPort) + { + _clientFactory = new ClientFactory(); + + // register generated IGenericCalculator proxy, see MyGrpcProxies + _clientFactory.AddGenericCalculatorInt32Client(); + + _channel = new Channel("localhost", serverPort, ChannelCredentials.Insecure); + } + + public async Task InvokeGenericCalculator() + { + // create instance of GenericCalculatorInt32Client, see MyGrpcProxies + var proxy = _clientFactory.CreateClient>(_channel); + + // POST /IGenericCalculator-Int32/Touch + Console.WriteLine("Invoke Touch"); + var touchResponse = proxy.Touch(); + Console.WriteLine(" {0}", touchResponse); + + // POST /IGenericCalculator-Int32/GetRandomValue + Console.WriteLine("Invoke GetRandomValue"); + var x = await proxy.GetRandomValue(); + var y = await proxy.GetRandomValue(); + Console.WriteLine(" X = {0}", x); + Console.WriteLine(" Y = {0}", y); + + // POST /IGenericCalculator-Int32/Sum + Console.WriteLine("Invoke Sum"); + var sumResponse = await proxy.Sum(x, y); + Console.WriteLine(" {0} + {1} = {2}", x, y, sumResponse); + + // POST /IGenericCalculator-Int32/Multiply + Console.WriteLine("Invoke Multiply"); + var multiplyResponse = await proxy.Multiply(x, y); + Console.WriteLine(" {0} * {1} = {2}", x, y, multiplyResponse); + } + + public async Task InvokeDoubleCalculator() + { + // proxy will be generated on-fly + var proxy = _clientFactory.CreateClient(_channel); + + // POST /IDoubleCalculator/Touch + Console.WriteLine("Invoke Touch"); + var touchResponse = proxy.Touch(); + Console.WriteLine(" {0}", touchResponse); + + // POST /IDoubleCalculator/GetRandomValue + Console.WriteLine("Invoke GetRandomValue"); + var x = await proxy.GetRandomValue(); + var y = await proxy.GetRandomValue(); + Console.WriteLine(" X = {0}", x); + Console.WriteLine(" Y = {0}", y); + + // POST /IDoubleCalculator/Sum + Console.WriteLine("Invoke Sum"); + var sumResponse = await proxy.Sum(x, y); + Console.WriteLine(" {0} + {1} = {2}", x, y, sumResponse); + + // POST /IDoubleCalculator/Multiply + Console.WriteLine("Invoke Multiply"); + var multiplyResponse = await proxy.Multiply(x, y); + Console.WriteLine(" {0} * {1} = {2}", x, y, multiplyResponse); + } + } +} diff --git a/Examples/InterfaceInheritance/Client/MyGrpcProxies.cs b/Examples/InterfaceInheritance/Client/MyGrpcProxies.cs new file mode 100644 index 00000000..9dda806c --- /dev/null +++ b/Examples/InterfaceInheritance/Client/MyGrpcProxies.cs @@ -0,0 +1,10 @@ +using Contract; +using ServiceModel.Grpc.DesignTime; + +namespace Client +{ + [ImportGrpcService(typeof(IGenericCalculator))] // configure ServiceModel.Grpc.DesignTime to generate a source code for IGenericCalculator client proxy + internal static partial class MyGrpcProxies + { + } +} diff --git a/Examples/InterfaceInheritance/Contract/Contract.csproj b/Examples/InterfaceInheritance/Contract/Contract.csproj new file mode 100644 index 00000000..b3cbecc2 --- /dev/null +++ b/Examples/InterfaceInheritance/Contract/Contract.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + + + + + + + + diff --git a/Examples/InterfaceInheritance/Contract/ICalculator{TValue}.cs b/Examples/InterfaceInheritance/Contract/ICalculator{TValue}.cs new file mode 100644 index 00000000..bc3e5e77 --- /dev/null +++ b/Examples/InterfaceInheritance/Contract/ICalculator{TValue}.cs @@ -0,0 +1,17 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +// ReSharper disable OperationContractWithoutServiceContract + +namespace Contract +{ + // remove [ServiceContract] + public interface ICalculator : IRemoteService + { + [OperationContract] + Task Sum(TValue x, TValue y); + + [OperationContract] + ValueTask Multiply(TValue x, TValue y); + } +} diff --git a/Examples/InterfaceInheritance/Contract/IDoubleCalculator.cs b/Examples/InterfaceInheritance/Contract/IDoubleCalculator.cs new file mode 100644 index 00000000..dcd70730 --- /dev/null +++ b/Examples/InterfaceInheritance/Contract/IDoubleCalculator.cs @@ -0,0 +1,12 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace Contract +{ + [ServiceContract] + public interface IDoubleCalculator : ICalculator + { + [OperationContract] + ValueTask GetRandomValue(); + } +} diff --git a/Examples/InterfaceInheritance/Contract/IGenericCalculator{TValue}.cs b/Examples/InterfaceInheritance/Contract/IGenericCalculator{TValue}.cs new file mode 100644 index 00000000..7a4912f6 --- /dev/null +++ b/Examples/InterfaceInheritance/Contract/IGenericCalculator{TValue}.cs @@ -0,0 +1,12 @@ +using System.ServiceModel; +using System.Threading.Tasks; + +namespace Contract +{ + [ServiceContract] + public interface IGenericCalculator : ICalculator + { + [OperationContract] + ValueTask GetRandomValue(); + } +} diff --git a/Examples/InterfaceInheritance/Contract/IRemoteService.cs b/Examples/InterfaceInheritance/Contract/IRemoteService.cs new file mode 100644 index 00000000..d072fbfb --- /dev/null +++ b/Examples/InterfaceInheritance/Contract/IRemoteService.cs @@ -0,0 +1,13 @@ +// ReSharper disable OperationContractWithoutServiceContract + +using System.ServiceModel; + +namespace Contract +{ + // remove [ServiceContract] + public interface IRemoteService + { + [OperationContract] + string Touch(); + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj new file mode 100644 index 00000000..c6622ca0 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj.user b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj.user new file mode 100644 index 00000000..b7a03ad9 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Demo.ServerAspNetCore.csproj.user @@ -0,0 +1,9 @@ + + + + ServerAspNetCore + + + ProjectDebugger + + \ No newline at end of file diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/MyGrpcServices.cs b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/MyGrpcServices.cs new file mode 100644 index 00000000..f6684e7c --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/MyGrpcServices.cs @@ -0,0 +1,10 @@ +using Service; +using ServiceModel.Grpc.DesignTime; + +namespace Demo.ServerAspNetCore +{ + [ExportGrpcService(typeof(GenericCalculator), GenerateAspNetExtensions = true)] // configure ServiceModel.Grpc.DesignTime to generate a source code for IGenericCalculator endpoint + internal static partial class MyGrpcServices + { + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Program.cs b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Program.cs new file mode 100644 index 00000000..ac1352ef --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Program.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Client; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Demo.ServerAspNetCore +{ + public static class Program + { + public static async Task Main() + { + using (var host = await StartWebHost()) + { + var calls = new ClientCalls(5000); + + await calls.InvokeGenericCalculator(); + await calls.InvokeDoubleCalculator(); + + if (Debugger.IsAttached) + { + Console.WriteLine("..."); + Console.ReadLine(); + } + + await host.StopAsync(); + } + } + + private static async Task StartWebHost() + { + var host = Host + .CreateDefaultBuilder() + .ConfigureAppConfiguration(builder => + { + builder.AddJsonFile(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"), false, false); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .Build(); + + await host.StartAsync(); + return host; + } + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Properties/launchSettings.json b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Properties/launchSettings.json new file mode 100644 index 00000000..3e203b90 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ServerAspNetCore": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Startup.cs b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Startup.cs new file mode 100644 index 00000000..f9cf0aa9 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/Startup.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Service; + +namespace Demo.ServerAspNetCore +{ + internal sealed class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddServiceModelGrpc(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + // register generated GenericCalculatorInt32Endpoint, see MyGrpcServices + endpoints.MapGenericCalculatorInt32(); + + // endpoint will be generated on-fly + endpoints.MapGrpcService(); + }); + } + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerAspNetCore/appsettings.json b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/appsettings.json new file mode 100644 index 00000000..3e5d8d47 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerAspNetCore/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http2": { + "Url": "http://*:5000", + "Protocols": "Http2" + } + } + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerSelfHost/Demo.ServerSelfHost.csproj b/Examples/InterfaceInheritance/Demo.ServerSelfHost/Demo.ServerSelfHost.csproj new file mode 100644 index 00000000..51df0ecd --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerSelfHost/Demo.ServerSelfHost.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + + diff --git a/Examples/InterfaceInheritance/Demo.ServerSelfHost/MyGrpcServices.cs b/Examples/InterfaceInheritance/Demo.ServerSelfHost/MyGrpcServices.cs new file mode 100644 index 00000000..a1129aa6 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerSelfHost/MyGrpcServices.cs @@ -0,0 +1,10 @@ +using Service; +using ServiceModel.Grpc.DesignTime; + +namespace Demo.ServerSelfHost +{ + [ExportGrpcService(typeof(DoubleCalculator), GenerateSelfHostExtensions = true)] // configure ServiceModel.Grpc.DesignTime to generate a source code for DoubleCalculator endpoint + internal static partial class MyGrpcServices + { + } +} diff --git a/Examples/InterfaceInheritance/Demo.ServerSelfHost/Program.cs b/Examples/InterfaceInheritance/Demo.ServerSelfHost/Program.cs new file mode 100644 index 00000000..ee191b65 --- /dev/null +++ b/Examples/InterfaceInheritance/Demo.ServerSelfHost/Program.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Client; +using Grpc.Core; +using Service; + +namespace Demo.ServerSelfHost +{ + public static class Program + { + public static async Task Main() + { + var server = new Server + { + Ports = + { + new ServerPort("localhost", 5001, ServerCredentials.Insecure) + } + }; + + // register generated DoubleCalculatorEndpoint, see MyGrpcServices + server.Services.AddDoubleCalculator(new DoubleCalculator()); + + // endpoint will be generated on-fly + server.Services.AddServiceModelSingleton(new GenericCalculator()); + + server.Start(); + + var calls = new ClientCalls(5001); + + await calls.InvokeGenericCalculator(); + await calls.InvokeDoubleCalculator(); + + if (Debugger.IsAttached) + { + Console.WriteLine("..."); + Console.ReadLine(); + } + + await server.ShutdownAsync(); + } + } +} diff --git a/Examples/InterfaceInheritance/InterfaceInheritance.sln b/Examples/InterfaceInheritance/InterfaceInheritance.sln new file mode 100644 index 00000000..2af40dd6 --- /dev/null +++ b/Examples/InterfaceInheritance/InterfaceInheritance.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{03B81E60-4D4E-40A1-8127-282FBF2C02B7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.ServerAspNetCore", "Demo.ServerAspNetCore\Demo.ServerAspNetCore.csproj", "{B1848899-C1BF-45D0-B5AF-02F8A84432EF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contract", "Contract\Contract.csproj", "{87F9282A-F9FB-4D83-8FF2-E9CBF466D852}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{5BA72E91-FDBC-4614-B6FA-FB362986FE63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{6221A00D-16D5-48BD-A23C-E350FDA1F286}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.ServerSelfHost", "Demo.ServerSelfHost\Demo.ServerSelfHost.csproj", "{44DFB5FA-644C-442B-B765-0BB7715615DE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {03B81E60-4D4E-40A1-8127-282FBF2C02B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03B81E60-4D4E-40A1-8127-282FBF2C02B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03B81E60-4D4E-40A1-8127-282FBF2C02B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03B81E60-4D4E-40A1-8127-282FBF2C02B7}.Release|Any CPU.Build.0 = Release|Any CPU + {B1848899-C1BF-45D0-B5AF-02F8A84432EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1848899-C1BF-45D0-B5AF-02F8A84432EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1848899-C1BF-45D0-B5AF-02F8A84432EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1848899-C1BF-45D0-B5AF-02F8A84432EF}.Release|Any CPU.Build.0 = Release|Any CPU + {87F9282A-F9FB-4D83-8FF2-E9CBF466D852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87F9282A-F9FB-4D83-8FF2-E9CBF466D852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87F9282A-F9FB-4D83-8FF2-E9CBF466D852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87F9282A-F9FB-4D83-8FF2-E9CBF466D852}.Release|Any CPU.Build.0 = Release|Any CPU + {6221A00D-16D5-48BD-A23C-E350FDA1F286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6221A00D-16D5-48BD-A23C-E350FDA1F286}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6221A00D-16D5-48BD-A23C-E350FDA1F286}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6221A00D-16D5-48BD-A23C-E350FDA1F286}.Release|Any CPU.Build.0 = Release|Any CPU + {44DFB5FA-644C-442B-B765-0BB7715615DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44DFB5FA-644C-442B-B765-0BB7715615DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44DFB5FA-644C-442B-B765-0BB7715615DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44DFB5FA-644C-442B-B765-0BB7715615DE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {03B81E60-4D4E-40A1-8127-282FBF2C02B7} = {5BA72E91-FDBC-4614-B6FA-FB362986FE63} + {87F9282A-F9FB-4D83-8FF2-E9CBF466D852} = {5BA72E91-FDBC-4614-B6FA-FB362986FE63} + {6221A00D-16D5-48BD-A23C-E350FDA1F286} = {5BA72E91-FDBC-4614-B6FA-FB362986FE63} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8BFD924E-DE25-47E7-8F8F-E05C26A9F434} + EndGlobalSection +EndGlobal diff --git a/Examples/InterfaceInheritance/Service/DoubleCalculator.cs b/Examples/InterfaceInheritance/Service/DoubleCalculator.cs new file mode 100644 index 00000000..b9fff3ce --- /dev/null +++ b/Examples/InterfaceInheritance/Service/DoubleCalculator.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Contract; + +namespace Service +{ + public sealed class DoubleCalculator : IDoubleCalculator + { + // POST /IDoubleCalculator/Touch + public string Touch() + { + return nameof(DoubleCalculator); + } + + // POST /IDoubleCalculator/Sum + public Task Sum(double x, double y) + { + return Task.FromResult(x + y); + } + + // POST /IDoubleCalculator/Multiply + public ValueTask Multiply(double x, double y) + { + return new ValueTask(x * y); + } + + // POST /IDoubleCalculator/GetRandomValue + public ValueTask GetRandomValue() + { + var result = new Random(DateTime.Now.Millisecond).NextDouble(); + return new ValueTask(result); + } + } +} diff --git a/Examples/InterfaceInheritance/Service/GenericCalculator{TValue}.cs b/Examples/InterfaceInheritance/Service/GenericCalculator{TValue}.cs new file mode 100644 index 00000000..5e2ecb3e --- /dev/null +++ b/Examples/InterfaceInheritance/Service/GenericCalculator{TValue}.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Contract; + +namespace Service +{ + public sealed class GenericCalculator : IGenericCalculator + { + private static readonly Func Cast; + private static readonly Func DoSum; + private static readonly Func DoMultiply; + + static GenericCalculator() + { + var x = Expression.Parameter(typeof(TValue)); + var y = Expression.Parameter(typeof(TValue)); + + DoSum = Expression + .Lambda>( + Expression.Add(x, y), + x, + y) + .Compile(); + + DoMultiply = Expression + .Lambda>( + Expression.Multiply(x, y), + x, + y) + .Compile(); + + var value = Expression.Parameter(typeof(int)); + Cast = Expression + .Lambda>( + Expression.Convert(value, typeof(TValue)), + value) + .Compile(); + } + + // POST /IGenericCalculator-TValue/Touch + public string Touch() + { + return string.Format("GenericCalculator<{0}>", typeof(TValue).Name); + } + + // POST /IGenericCalculator-TValue/Sum + public Task Sum(TValue x, TValue y) + { + var result = DoSum(x, y); + return Task.FromResult(result); + } + + // POST /IGenericCalculator-TValue/Multiply + public ValueTask Multiply(TValue x, TValue y) + { + var result = DoMultiply(x, y); + return new ValueTask(result); + } + + // POST /IGenericCalculator-TValue/GetRandomValue + public ValueTask GetRandomValue() + { + var value = new Random(DateTime.Now.Millisecond).Next(0, 10_000); + var result = Cast(value); + return new ValueTask(result); + } + } +} diff --git a/Examples/InterfaceInheritance/Service/Service.csproj b/Examples/InterfaceInheritance/Service/Service.csproj new file mode 100644 index 00000000..57b6637c --- /dev/null +++ b/Examples/InterfaceInheritance/Service/Service.csproj @@ -0,0 +1,11 @@ + + + + net6.0 + + + + + + + From 84d54501047924269101280129725c5de3e60789 Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sun, 30 Jan 2022 16:10:10 +0100 Subject: [PATCH 4/6] shared interface: update docs --- docs/ServiceAndOperationBinding.md | 62 +++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/ServiceAndOperationBinding.md b/docs/ServiceAndOperationBinding.md index d3f41105..f2554f15 100644 --- a/docs/ServiceAndOperationBinding.md +++ b/docs/ServiceAndOperationBinding.md @@ -67,4 +67,64 @@ static void Call() // POST /IScientificCalculator/Multiply MyDefaultClientFactory.CreateClient(channel).Multiply(...); } -``` \ No newline at end of file +``` + +## Contract inheritance + +Inheritance of interfaces, defined as `ServiceContract`, does not affect operation bindings. + +``` c# +[ServiceContract] +public interface ICalculator +{ + [OperationContract] + int Sum(int x, int y); +} + +[ServiceContract] +public interface IScientificCalculator : ICalculator +{ + [OperationContract] + int Multiply(int x, int y); +} + +internal sealed class ScientificCalculator : IScientificCalculator +{ + // accept POST /ICalculator/Sum + public int Sum(int x, int y) { /*...*/ } + + // accept POST /IScientificCalculator/Multiply + public int Multiply(int x, int y) { /*...*/ } +} +``` + +## Interface inheritance + +If an interface is not marked as `ServiceContract`, the service name for each defined operation comes from the top `ServiceContract` interface. + +``` c# +public interface ICalculator +{ + [OperationContract] + int Sum(int x, int y); +} + +[ServiceContract] +public interface IScientificCalculator : ICalculator +{ + [OperationContract] + int Multiply(int x, int y); +} + +internal sealed class ScientificCalculator : IScientificCalculator +{ + // accept POST /IScientificCalculator/Sum + public int Sum(int x, int y) { /*...*/ } + + // accept POST /IScientificCalculator/Multiply + public int Multiply(int x, int y) { /*...*/ } +} +``` + +[View InterfaceInheritance](https://github.com/max-ieremenko/ServiceModel.Grpc/tree/master/Examples/InterfaceInheritance) example. + From 08579699da605666f9a580e5102f1e9d869a01f8 Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sat, 5 Feb 2022 17:00:45 +0100 Subject: [PATCH 5/6] code generator: fix IsAssignableFrom --- .../Internal/InterfaceTreeTest.Domain.cs | 20 +++++++++++++++++++ .../Generator/Internal/InterfaceTreeTest.cs | 20 +++++++++++++++++++ .../Generator/Internal/SyntaxTools.cs | 4 ++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs index 7f67dc42..abf4ebd4 100644 --- a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs +++ b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.Domain.cs @@ -131,5 +131,25 @@ public interface IContract : IService2 { } } + + public static class TransientGenericInterface + { + public interface IService1 + { + [OperationContract] + void Method1(); + } + + public interface IService2 : IService1 + { + [OperationContract] + void Method2(T value); + } + + [ServiceContract] + public interface IContract : IService2 + { + } + } } } diff --git a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs index c6458f11..e1330ccf 100644 --- a/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs +++ b/Sources/ServiceModel.Grpc.DesignTime.Test/Generator/Internal/InterfaceTreeTest.cs @@ -30,6 +30,7 @@ public partial class InterfaceTreeTest nameof(InterfaceTreeTest), references: new[] { + MetadataReference.CreateFromFile(typeof(int).Assembly.Location), MetadataReference.CreateFromFile(typeof(InterfaceTreeTest).Assembly.Location) }); @@ -128,5 +129,24 @@ public void TransientInterfaceTree() sut.Services[1].ServiceName.ShouldBe(nameof(TransientInterface.IContract)); sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientInterface.IService1))); } + + [Test] + public void TransientGenericInterfaceTree() + { + var rootType = Compilation.GetTypeByMetadataName(typeof(TransientGenericInterface.IContract)); + var sut = new InterfaceTree(rootType); + + sut.Interfaces.ShouldBeEmpty(); + sut.Services.Count.ShouldBe(3); + + sut.Services[0].ServiceName.ShouldBe("IContract-Int32"); + sut.Services[0].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientGenericInterface.IContract))); + + sut.Services[1].ServiceName.ShouldBe("IContract-Int32"); + sut.Services[1].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientGenericInterface.IService2))); + + sut.Services[2].ServiceName.ShouldBe("IContract-Int32"); + sut.Services[2].ServiceType.ShouldBe(Compilation.GetTypeByMetadataName(typeof(TransientGenericInterface.IService1))); + } } } diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs index 5d95a948..ce6177f7 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/SyntaxTools.cs @@ -181,7 +181,7 @@ public static bool IsAssignableFrom(this ITypeSymbol type, Type expected) return true; } - foreach (var i in type.Interfaces) + foreach (var i in type.AllInterfaces) { if (IsMatch(i, expected)) { @@ -210,7 +210,7 @@ public static bool IsAssignableFrom(this ITypeSymbol type, ITypeSymbol expected) return true; } - foreach (var i in expected.Interfaces) + foreach (var i in expected.AllInterfaces) { if (SymbolEqualityComparer.Default.Equals(i, type)) { From 578bd680b13d048036093b4d48104c846555840b Mon Sep 17 00:00:00 2001 From: max-ieremenko <> Date: Sat, 5 Feb 2022 17:01:42 +0100 Subject: [PATCH 6/6] code generator: add [ExcludeFromCodeCoverage], [Obfuscation(Exclude = true)] --- .../Generator/CSharpSourceGenerator.cs | 6 +++++- .../Generator/Internal/CSharp/CodeGeneratorBase.cs | 4 +++- .../ServiceModel.Grpc.DesignTime.csproj | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/CSharpSourceGenerator.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/CSharpSourceGenerator.cs index b4f084c9..2248d467 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/Generator/CSharpSourceGenerator.cs +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/CSharpSourceGenerator.cs @@ -1,5 +1,5 @@ // -// Copyright 2021 Max Ieremenko +// Copyright 2021-2022 Max Ieremenko // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ using System; using System.CodeDom.Compiler; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading; @@ -119,6 +121,8 @@ private static IEnumerable GetSharedUsing() yield return typeof(Message).Namespace!; yield return typeof(CompilerGeneratedAttribute).Namespace!; yield return typeof(GeneratedCodeAttribute).Namespace!; + yield return typeof(ExcludeFromCodeCoverageAttribute).Namespace!; + yield return typeof(ObfuscationAttribute).Namespace!; } private static void ShowWarnings(Logger logger, ContractDescription contract, INamedTypeSymbol serviceType) diff --git a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/CSharp/CodeGeneratorBase.cs b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/CSharp/CodeGeneratorBase.cs index dfc2c2b8..c6d94dd1 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/CSharp/CodeGeneratorBase.cs +++ b/Sources/ServiceModel.Grpc.DesignTime/Generator/Internal/CSharp/CodeGeneratorBase.cs @@ -41,7 +41,9 @@ protected void WriteMetadata() .Append("[GeneratedCode(\"ServiceModel.Grpc\", \"") .Append(GetType().Assembly.GetName().Version.ToString(3)) .AppendLine("\")]") - .AppendLine("[CompilerGenerated]"); + .AppendLine("[CompilerGenerated]") + .AppendLine("[ExcludeFromCodeCoverage]") + .AppendLine("[Obfuscation(Exclude = true)]"); } } } diff --git a/Sources/ServiceModel.Grpc.DesignTime/ServiceModel.Grpc.DesignTime.csproj b/Sources/ServiceModel.Grpc.DesignTime/ServiceModel.Grpc.DesignTime.csproj index 71c3894a..036a16f0 100644 --- a/Sources/ServiceModel.Grpc.DesignTime/ServiceModel.Grpc.DesignTime.csproj +++ b/Sources/ServiceModel.Grpc.DesignTime/ServiceModel.Grpc.DesignTime.csproj @@ -3,6 +3,7 @@ netstandard2.0 ServiceModel.Grpc.DesignTime + true