Skip to content

Commit 89775fb

Browse files
author
Meir Kriheli
committed
Merge remote-tracking branch 'origin/main' into meirk/reflection_inherited_and_generics_methods
# Conflicts: # src/protobuf-net.Grpc.Reflection/SchemaGenerator.cs # tests/protobuf-net.Grpc.Reflection.Test/SchemaGeneration.cs
2 parents c954280 + 5148278 commit 89775fb

File tree

3 files changed

+182
-40
lines changed

3 files changed

+182
-40
lines changed

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- [Getting Started](gettingstarted)
44
- [Configuration Options](configuration)
55
- [Package Layout](projects)
6+
- [Build Tools ("code-first" focus)](https://protobuf-net.github.io/protobuf-net/build_tools)
7+
- [Build Tools ("contract-first" focus)](https://protobuf-net.github.io/protobuf-net/contract_first)
68
- [Create Proto File](createProtoFile)
79

810
Other Content

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.Linq;
78
using System.Reflection;
89

@@ -26,68 +27,101 @@ public sealed class SchemaGenerator
2627
/// <summary>
2728
/// Get the .proto schema associated with a service contract
2829
/// </summary>
30+
/// <typeparam name="TService">The service type to generate schema for.</typeparam>
2931
/// <remarks>This API is considered experimental and may change slightly</remarks>
3032
public string GetSchema<TService>()
3133
=> GetSchema(typeof(TService));
3234

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

46-
name = ServiceBinder.GetNameParts(name, contractType, out var package);
47-
var service = new Service
48-
{
49-
Name = name
50-
};
67+
name = ServiceBinder.GetNameParts(name, contractType, out var package);
68+
// currently we allow only services from same package, to be output to single proto file
69+
if (!string.IsNullOrEmpty(globalPackage)
70+
&& package != globalPackage)
71+
{
72+
throw new ArgumentException(
73+
$"All services must be of the same package! '{contractType.Name}' is from package '{package}' while previous package: {globalPackage}",
74+
nameof(contractTypes));
75+
}
76+
globalPackage = package;
77+
78+
var service = new Service
79+
{
80+
Name = name
81+
};
5182

52-
var ops = GetMethodsRecursively(binder, contractType);
53-
foreach (var method in ops)
54-
{
55-
if (method.DeclaringType == typeof(object))
56-
{ /* skip */ }
57-
else if (ContractOperation.TryIdentifySignature(method, binderConfiguration, out var op, null))
83+
var ops = GetMethodsRecursively(binder, contractType);
84+
foreach (var method in ops)
5885
{
59-
service.Methods.Add(
60-
new ServiceMethod
61-
{
62-
Name = op.Name,
63-
InputType = ApplySubstitutes(op.From),
64-
OutputType = ApplySubstitutes(op.To),
65-
ClientStreaming = op.MethodType switch
66-
{
67-
MethodType.ClientStreaming => true,
68-
MethodType.DuplexStreaming => true,
69-
_ => false,
70-
},
71-
ServerStreaming = op.MethodType switch
86+
if (method.DeclaringType == typeof(object))
87+
{
88+
/* skip */
89+
}
90+
else if (ContractOperation.TryIdentifySignature(method, binderConfiguration, out var op, null))
91+
{
92+
service.Methods.Add(
93+
new ServiceMethod
7294
{
73-
MethodType.ServerStreaming => true,
74-
MethodType.DuplexStreaming => true,
75-
_ => false,
76-
},
77-
}
78-
);
95+
Name = op.Name,
96+
InputType = ApplySubstitutes(op.From),
97+
OutputType = ApplySubstitutes(op.To),
98+
ClientStreaming = op.MethodType switch
99+
{
100+
MethodType.ClientStreaming => true,
101+
MethodType.DuplexStreaming => true,
102+
_ => false,
103+
},
104+
ServerStreaming = op.MethodType switch
105+
{
106+
MethodType.ServerStreaming => true,
107+
MethodType.DuplexStreaming => true,
108+
_ => false,
109+
},
110+
}
111+
);
112+
}
79113
}
114+
115+
service.Methods.Sort((x, y) => string.Compare(x.Name, y.Name)); // make it predictable
116+
services.Add(service);
80117
}
81-
service.Methods.Sort((x, y) => string.Compare(x.Name, y.Name)); // make it predictable
118+
82119
var options = new SchemaGenerationOptions
83120
{
84121
Syntax = ProtoSyntax,
85-
Package = package,
86-
Services =
87-
{
88-
service
89-
}
122+
Package = globalPackage,
90123
};
124+
options.Services.AddRange(services);
91125

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

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Linq;
99
using System.Runtime.Serialization;
1010
using System.Threading.Tasks;
11+
using protobuf_net.Grpc.Reflection.Test;
1112
using Google.Protobuf.Reflection;
1213
using Grpc.Core;
1314
using Xunit;
@@ -301,12 +302,117 @@ service ConferencesService {
301302
", proto, ignoreLineEndingDifferences: true);
302303
}
303304

305+
public interface INotAService
306+
{
307+
ValueTask<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default);
308+
}
304309
[Fact]
305310
public void WhenInterfaceIsNotServiceContract_Throw()
306311
{
307312
var generator = new SchemaGenerator();
308313
Action activation = () => generator.GetSchema<INotAService>();
309314
Assert.Throws<ArgumentException>(activation.Invoke);
310315
}
316+
317+
// ReSharper disable once ClassNeverInstantiated.Global
318+
public class NotAService
319+
{
320+
public Task<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default)
321+
=> Task.FromResult(new MyResponse());
322+
}
323+
[Fact]
324+
public void WhenClassIsNotServiceContract_Throw()
325+
{
326+
var generator = new SchemaGenerator();
327+
Action activation = () => generator.GetSchema<NotAService>();
328+
Assert.Throws<ArgumentException>(activation.Invoke);
329+
}
330+
331+
[Service]
332+
public interface ISimpleService1
333+
{
334+
ValueTask<MyResponse> SomeMethod1(MyRequest request, CallContext callContext = default);
335+
}
336+
[Service]
337+
public interface ISimpleService2
338+
{
339+
ValueTask<MyResponse> SomeMethod2(MyRequest request, CallContext callContext = default);
340+
}
341+
342+
/// <summary>
343+
/// When we have multiple services which share same classes,
344+
/// we would like to have a schema which defines those services
345+
/// while having their shared classes defined only once - in that schema.
346+
/// The proto schema consumer will generate code (in any language) while the classes are common to the several services
347+
/// and can be reused towards multiple services, the same way the code-first uses those shared classes..
348+
/// </summary>
349+
[Fact]
350+
public void MultiServicesInSameSchema_ServicesAreFromSameNamespace_Success()
351+
{
352+
var generator = new SchemaGenerator();
353+
354+
var proto = generator.GetSchema(typeof(ISimpleService1), typeof(ISimpleService2));
355+
356+
Assert.Equal(@"syntax = ""proto3"";
357+
package protobuf_net.Grpc.Reflection.Test;
358+
import ""google/protobuf/timestamp.proto"";
359+
360+
enum Category {
361+
Default = 0;
362+
Foo = 1;
363+
Bar = 2;
364+
}
365+
message MyRequest {
366+
int32 Id = 1;
367+
.google.protobuf.Timestamp When = 2;
368+
}
369+
message MyResponse {
370+
string Value = 1;
371+
Category Category = 2;
372+
string RefId = 3; // default value could not be applied: 00000000-0000-0000-0000-000000000000
373+
}
374+
service SimpleService1 {
375+
rpc SomeMethod1 (MyRequest) returns (MyResponse);
376+
}
377+
service SimpleService2 {
378+
rpc SomeMethod2 (MyRequest) returns (MyResponse);
379+
}
380+
", proto, ignoreLineEndingDifferences: true);
381+
}
382+
383+
384+
/// <summary>
385+
/// When we have multiple services but with different namespaces,
386+
/// since schema should export a single package, this situation is unsupported.
387+
/// We expect for an exception.
388+
/// </summary>
389+
[Fact]
390+
public void MultiServicesInSameSchema_ServicesAreFromDifferentNamespaces_Throw()
391+
{
392+
var generator = new SchemaGenerator();
393+
394+
Action activation = () => generator.GetSchema(typeof(DifferentNamespace1.IServiceInNamespace1), typeof(DifferentNamespace2.IServiceInNamespace2));
395+
Assert.Throws<ArgumentException>(activation.Invoke);
396+
}
311397
}
312398
}
399+
400+
401+
namespace DifferentNamespace1
402+
{
403+
404+
[Service]
405+
public interface IServiceInNamespace1
406+
{
407+
ValueTask<SchemaGeneration.MyResponse> SomeMethod1(SchemaGeneration.MyRequest request, CallContext callContext = default);
408+
}
409+
}
410+
namespace DifferentNamespace2
411+
{
412+
[Service]
413+
public interface IServiceInNamespace2
414+
{
415+
ValueTask<SchemaGeneration.MyResponse> SomeMethod2(SchemaGeneration.MyRequest request, CallContext callContext = default);
416+
}
417+
418+
}

0 commit comments

Comments
 (0)