Skip to content

Commit 5148278

Browse files
meirkrMeir Kriheli
andauthored
Reflection - allow generating multi services with their shared classes defined once. (#209)
* allow generating multo services with their shared classes defined once. Added more unit tests to cover this feature. Added an addition unit test of exception in case schema gets a non service contract interface/class. * code review - don't break backward (runtime) compatibility. Add back the existing GetSchema with singele type as argument, which actually calls to the new form of params types. * add attention remarks no to delete the method although it looks as covered by its following overloading method. Co-authored-by: Meir Kriheli <[email protected]>
1 parent b052630 commit 5148278

File tree

2 files changed

+189
-41
lines changed

2 files changed

+189
-41
lines changed

src/protobuf-net.Grpc.Reflection/SchemaGenerator.cs

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ProtoBuf.Grpc.Internal;
44
using ProtoBuf.Meta;
55
using System;
6+
using System.Collections.Generic;
67
using System.Reflection;
78

89
namespace ProtoBuf.Grpc.Reflection
@@ -25,67 +26,100 @@ public sealed class SchemaGenerator
2526
/// <summary>
2627
/// Get the .proto schema associated with a service contract
2728
/// </summary>
29+
/// <typeparam name="TService">The service type to generate schema for.</typeparam>
2830
/// <remarks>This API is considered experimental and may change slightly</remarks>
2931
public string GetSchema<TService>()
3032
=> GetSchema(typeof(TService));
3133

3234
/// <summary>
3335
/// Get the .proto schema associated with a service contract
3436
/// </summary>
35-
/// <remarks>This API is considered experimental and may change slightly</remarks>
37+
/// <param name="contractType">The service type to generate schema for.</param>
38+
/// <remarks>This API is considered experimental and may change slightly.
39+
/// ATTENTION! although the 'GetSchema(params Type[] contractTypes)' covers also a case of 'GetSchema(Type contractType)',
40+
/// this method need to remain for backward compatibility for client which will get this updated version, without recompilation.
41+
/// Thus, this method mustn't be deleted.</remarks>
3642
public string GetSchema(Type contractType)
43+
=> GetSchema(new [] {contractType});
44+
45+
/// <summary>
46+
/// Get the .proto schema associated with multiple service contracts
47+
/// </summary>
48+
/// <param name="contractTypes">Array (or params syntax) of service types to generate schema for.</param>
49+
/// <remarks>This API is considered experimental and may change slightly
50+
/// All types will be generated into single schema.
51+
/// All the shared classes the services use will be generated only once for all of them.</remarks>
52+
public string GetSchema(params Type[] contractTypes)
3753
{
54+
string globalPackage = "";
55+
List<Service> services = new List<Service>();
3856
var binderConfiguration = BinderConfiguration ?? BinderConfiguration.Default;
3957
var binder = binderConfiguration.Binder;
40-
if (!binder.IsServiceContract(contractType, out var name))
58+
foreach (var contractType in contractTypes)
4159
{
42-
throw new ArgumentException($"Type '{contractType.Name}' is not a service contract", nameof(contractType));
43-
}
60+
if (!binder.IsServiceContract(contractType, out var name))
61+
{
62+
throw new ArgumentException($"Type '{contractType.Name}' is not a service contract",
63+
nameof(contractTypes));
64+
}
4465

45-
name = ServiceBinder.GetNameParts(name, contractType, out var package);
46-
var service = new Service
47-
{
48-
Name = name
49-
};
50-
var ops = contractType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
51-
foreach (var method in ops)
52-
{
53-
if (method.DeclaringType == typeof(object))
54-
{ /* skip */ }
55-
else if (ContractOperation.TryIdentifySignature(method, binderConfiguration, out var op, null))
66+
name = ServiceBinder.GetNameParts(name, contractType, out var package);
67+
// currently we allow only services from same package, to be output to single proto file
68+
if (!string.IsNullOrEmpty(globalPackage)
69+
&& package != globalPackage)
5670
{
57-
service.Methods.Add(
58-
new ServiceMethod
59-
{
60-
Name = op.Name,
61-
InputType = ApplySubstitutes(op.From),
62-
OutputType = ApplySubstitutes(op.To),
63-
ClientStreaming = op.MethodType switch
64-
{
65-
MethodType.ClientStreaming => true,
66-
MethodType.DuplexStreaming => true,
67-
_ => false,
68-
},
69-
ServerStreaming = op.MethodType switch
71+
throw new ArgumentException(
72+
$"All services must be of the same package! '{contractType.Name}' is from package '{package}' while previous package: {globalPackage}",
73+
nameof(contractTypes));
74+
}
75+
globalPackage = package;
76+
77+
var service = new Service
78+
{
79+
Name = name
80+
};
81+
var ops = contractType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
82+
foreach (var method in ops)
83+
{
84+
if (method.DeclaringType == typeof(object))
85+
{
86+
/* skip */
87+
}
88+
else if (ContractOperation.TryIdentifySignature(method, binderConfiguration, out var op, null))
89+
{
90+
service.Methods.Add(
91+
new ServiceMethod
7092
{
71-
MethodType.ServerStreaming => true,
72-
MethodType.DuplexStreaming => true,
73-
_ => false,
74-
},
75-
}
76-
);
93+
Name = op.Name,
94+
InputType = ApplySubstitutes(op.From),
95+
OutputType = ApplySubstitutes(op.To),
96+
ClientStreaming = op.MethodType switch
97+
{
98+
MethodType.ClientStreaming => true,
99+
MethodType.DuplexStreaming => true,
100+
_ => false,
101+
},
102+
ServerStreaming = op.MethodType switch
103+
{
104+
MethodType.ServerStreaming => true,
105+
MethodType.DuplexStreaming => true,
106+
_ => false,
107+
},
108+
}
109+
);
110+
}
77111
}
112+
113+
service.Methods.Sort((x, y) => string.Compare(x.Name, y.Name)); // make it predictable
114+
services.Add(service);
78115
}
79-
service.Methods.Sort((x, y) => string.Compare(x.Name, y.Name)); // make it predictable
116+
80117
var options = new SchemaGenerationOptions
81118
{
82119
Syntax = ProtoSyntax,
83-
Package = package,
84-
Services =
85-
{
86-
service
87-
}
120+
Package = globalPackage,
88121
};
122+
options.Services.AddRange(services);
89123

90124
var model = binderConfiguration.MarshallerCache.TryGetFactory<ProtoBufMarshallerFactory>()?.Model ?? RuntimeTypeModel.Default;
91125
return model.GetSchema(options);

tests/protobuf-net.Grpc.Reflection.Test/SchemaGeneration.cs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Runtime.Serialization;
8-
using System.ServiceModel;
98
using System.Threading.Tasks;
9+
using protobuf_net.Grpc.Reflection.Test;
1010
using Xunit;
1111
using Xunit.Abstractions;
1212

@@ -151,5 +151,119 @@ service ConferencesService {
151151
}
152152
", proto, ignoreLineEndingDifferences: true);
153153
}
154+
155+
public interface INotAService
156+
{
157+
ValueTask<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default);
158+
}
159+
[Fact]
160+
public void WhenInterfaceIsNotServiceContract_Throw()
161+
{
162+
var generator = new SchemaGenerator();
163+
Action activation = () => generator.GetSchema<INotAService>();
164+
Assert.Throws<ArgumentException>(activation.Invoke);
165+
}
166+
167+
// ReSharper disable once ClassNeverInstantiated.Global
168+
public class NotAService
169+
{
170+
public Task<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default)
171+
=> Task.FromResult(new MyResponse());
172+
}
173+
[Fact]
174+
public void WhenClassIsNotServiceContract_Throw()
175+
{
176+
var generator = new SchemaGenerator();
177+
Action activation = () => generator.GetSchema<NotAService>();
178+
Assert.Throws<ArgumentException>(activation.Invoke);
179+
}
180+
181+
[Service]
182+
public interface ISimpleService1
183+
{
184+
ValueTask<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default);
185+
}
186+
[Service]
187+
public interface ISimpleService2
188+
{
189+
ValueTask<MyResponse> SomeMethod2(MyRequest request, CallContext callContext = default);
190+
}
191+
192+
/// <summary>
193+
/// When we have multiple services which share same classes,
194+
/// we would like to have a schema which defines those services
195+
/// while having their shared classes defined only once - in that schema.
196+
/// The proto schema consumer will generate code (in any language) while the classes are common to the several services
197+
/// and can be reused towards multiple services, the same way the code-first uses those shared classes..
198+
/// </summary>
199+
[Fact]
200+
public void MultiServicesInSameSchema_ServicesAreFromSameNamespace_Success()
201+
{
202+
var generator = new SchemaGenerator();
203+
204+
var proto = generator.GetSchema(typeof(ISimpleService1), typeof(ISimpleService2));
205+
206+
Assert.Equal(@"syntax = ""proto3"";
207+
package protobuf_net.Grpc.Reflection.Test;
208+
import ""google/protobuf/timestamp.proto"";
209+
210+
enum Category {
211+
Default = 0;
212+
Foo = 1;
213+
Bar = 2;
214+
}
215+
message MyRequest {
216+
int32 Id = 1;
217+
.google.protobuf.Timestamp When = 2;
218+
}
219+
message MyResponse {
220+
string Value = 1;
221+
Category Category = 2;
222+
string RefId = 3; // default value could not be applied: 00000000-0000-0000-0000-000000000000
223+
}
224+
service SimpleService1 {
225+
rpc SomeMethod1 (MyRequest) returns (MyResponse);
226+
}
227+
service SimpleService2 {
228+
rpc SomeMethod2 (MyRequest) returns (MyResponse);
229+
}
230+
", proto, ignoreLineEndingDifferences: true);
231+
}
232+
233+
234+
/// <summary>
235+
/// When we have multiple services but with different namespaces,
236+
/// since schema should export a single package, this situation is unsupported.
237+
/// We expect for an exception.
238+
/// </summary>
239+
[Fact]
240+
public void MultiServicesInSameSchema_ServicesAreFromDifferentNamespaces_Throw()
241+
{
242+
var generator = new SchemaGenerator();
243+
244+
Action activation = () => generator.GetSchema(typeof(DifferentNamespace1.IServiceInNamespace1), typeof(DifferentNamespace2.IServiceInNamespace2));
245+
Assert.Throws<ArgumentException>(activation.Invoke);
246+
}
247+
}
248+
}
249+
250+
251+
namespace DifferentNamespace1
252+
{
253+
254+
[Service]
255+
public interface IServiceInNamespace1
256+
{
257+
ValueTask<SchemaGeneration.MyResponse> SomeMethod1(SchemaGeneration.MyRequest request, CallContext callContext = default);
154258
}
155259
}
260+
namespace DifferentNamespace2
261+
{
262+
[Service]
263+
public interface IServiceInNamespace2
264+
{
265+
ValueTask<SchemaGeneration.MyResponse> SomeMethod2(SchemaGeneration.MyRequest request, CallContext callContext = default);
266+
}
267+
268+
}
269+

0 commit comments

Comments
 (0)