Skip to content

Commit 66ee196

Browse files
authored
Add SchemaGenerator API (#108)
* tweaks to reflection services code: - rename ServicesExtensions due to collision; combine two static classes - update Reflection.cs with latest bits - use the TypeModel that is configured on the binder, not necessarily the default - fixup intellisense - yak some IDE warnings * add SchemaGenerator API * fix bad merge
1 parent 4ed34e4 commit 66ee196

File tree

8 files changed

+230
-7
lines changed

8 files changed

+230
-7
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private static string GetType(BinderConfiguration binderConfiguration, Type type
8686
var fileName = type.FullName + ".proto";
8787
var fileDescriptor = fileDescriptorSet.Files.SingleOrDefault(f => f.Name.Equals(fileName, StringComparison.Ordinal));
8888

89-
TypeModel model = binderConfiguration.TryGetFactory(type) is ProtoBufMarshallerFactory factory ? factory.Model : RuntimeTypeModel.Default;
89+
TypeModel model = binderConfiguration.MarshallerCache.TryGetFactory(type) is ProtoBufMarshallerFactory factory ? factory.Model : RuntimeTypeModel.Default;
9090

9191
if (fileDescriptor is null)
9292
{
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Grpc.Core;
2+
using ProtoBuf.Grpc.Configuration;
3+
using ProtoBuf.Grpc.Internal;
4+
using ProtoBuf.Meta;
5+
using System;
6+
using System.Reflection;
7+
8+
namespace ProtoBuf.Grpc.Reflection
9+
{
10+
/// <summary>
11+
/// Allows creation of .proto schemas from service contracts
12+
/// </summary>
13+
public sealed class SchemaGenerator
14+
{
15+
/// <summary>
16+
/// Gets or sets the syntax version
17+
/// </summary>
18+
public ProtoSyntax ProtoSyntax { get; set; } = ProtoSyntax.Proto3;
19+
20+
/// <summary>
21+
/// Gets or sets the binder configuration (the default configuration is used if omitted)
22+
/// </summary>
23+
public BinderConfiguration? BinderConfiguration { get; set; }
24+
25+
/// <summary>
26+
/// Get the .proto schema associated with a service contract
27+
/// </summary>
28+
/// <remarks>This API is considered experimental and may change slightly</remarks>
29+
public string GetSchema<TService>()
30+
=> GetSchema(typeof(TService));
31+
32+
/// <summary>
33+
/// Get the .proto schema associated with a service contract
34+
/// </summary>
35+
/// <remarks>This API is considered experimental and may change slightly</remarks>
36+
public string GetSchema(Type contractType)
37+
{
38+
var binderConfiguration = BinderConfiguration ?? BinderConfiguration.Default;
39+
var binder = binderConfiguration.Binder;
40+
if (!binder.IsServiceContract(contractType, out var name))
41+
{
42+
throw new ArgumentException($"Type '{contractType.Name}' is not a service contract", nameof(contractType));
43+
}
44+
45+
string package = "";
46+
var idx = name!.LastIndexOf('.');
47+
if (idx >= 0)
48+
{
49+
package = name.Substring(0, idx);
50+
name = name.Substring(idx + 1);
51+
}
52+
var service = new Service
53+
{
54+
Name = name
55+
};
56+
var ops = contractType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
57+
foreach (var method in ops)
58+
{
59+
if (method.DeclaringType == typeof(object))
60+
{ /* skip */ }
61+
else if (ContractOperation.TryIdentifySignature(method, binderConfiguration, out var op, null))
62+
{
63+
service.Methods.Add(
64+
new ServiceMethod
65+
{
66+
Name = op.Name,
67+
InputType = ApplySubstitutes(op.From),
68+
OutputType = ApplySubstitutes(op.To),
69+
ClientStreaming = op.MethodType switch
70+
{
71+
MethodType.ClientStreaming => true,
72+
MethodType.DuplexStreaming => true,
73+
_ => false,
74+
},
75+
ServerStreaming = op.MethodType switch
76+
{
77+
MethodType.ServerStreaming => true,
78+
MethodType.DuplexStreaming => true,
79+
_ => false,
80+
},
81+
}
82+
);
83+
}
84+
}
85+
service.Methods.Sort((x, y) => string.Compare(x.Name, y.Name)); // make it predictable
86+
var options = new SchemaGenerationOptions
87+
{
88+
Syntax = ProtoSyntax,
89+
Package = package,
90+
Services =
91+
{
92+
service
93+
}
94+
};
95+
96+
var model = binderConfiguration.MarshallerCache.TryGetFactory<ProtoBufMarshallerFactory>()?.Model ?? RuntimeTypeModel.Default;
97+
return model.GetSchema(options);
98+
99+
static Type ApplySubstitutes(Type type)
100+
{
101+
#pragma warning disable CS0618 // Type or member is obsolete
102+
if (type == typeof(Empty)) return typeof(WellKnownTypes.Empty);
103+
#pragma warning restore CS0618 // Type or member is obsolete
104+
if (type == typeof(DateTime)) return typeof(WellKnownTypes.Timestamp);
105+
if (type == typeof(TimeSpan)) return typeof(WellKnownTypes.Duration);
106+
return type;
107+
}
108+
}
109+
}
110+
}

src/protobuf-net.Grpc.Reflection/protobuf-net.Grpc.Reflection.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="protobuf-net.Reflection" Version="3.0.17" />
11-
<PackageReference Include="protobuf-net" Version="3.0.17" />
12-
<PackageReference Include="protobuf-net.Core" Version="3.0.17" />
10+
<!-- note this uses v3 features that are not back-ported to v2 -->
11+
<PackageReference Include="protobuf-net.Reflection" Version="3.0.21" />
12+
<PackageReference Include="protobuf-net" Version="3.0.21" />
13+
<PackageReference Include="protobuf-net.Core" Version="3.0.21" />
1314
</ItemGroup>
1415

1516
<ItemGroup>

src/protobuf-net.Grpc/Configuration/ServiceBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public virtual bool IsServiceContract(Type contractType, out string? name)
8080
return true;
8181
}
8282

83-
string? serviceName = null;
83+
string? serviceName;
8484
var attribs = AttributeHelper.For(contractType, inherit: true);
8585
if (attribs.IsDefined("ProtoBuf.Grpc.Configuration.ServiceAttribute"))
8686
{

src/protobuf-net.Grpc/Internal/MarshallerCache.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,16 @@ internal void SetMarshaller<T>(Marshaller<T>? marshaller)
7777
}
7878
return null;
7979
}
80+
81+
internal TFactory? TryGetFactory<TFactory>()
82+
where TFactory : MarshallerFactory
83+
{
84+
foreach (var factory in _factories)
85+
{
86+
if (factory is TFactory typed)
87+
return typed;
88+
}
89+
return null;
90+
}
8091
}
8192
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using ProtoBuf;
2+
using ProtoBuf.Grpc;
3+
using ProtoBuf.Grpc.Reflection;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Runtime.Serialization;
7+
using System.ServiceModel;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
[module: CompatibilityLevel(CompatibilityLevel.Level300)] // configures how DateTime etc are handled
13+
14+
namespace protobuf_net.Grpc.Reflection.Test
15+
{
16+
public class SchemaGeneration
17+
{
18+
public SchemaGeneration(ITestOutputHelper log) => _log = log;
19+
private readonly ITestOutputHelper _log;
20+
private void Log(string message) => _log?.WriteLine(message);
21+
[Fact]
22+
public void CheckBasicSchema()
23+
{
24+
var generator = new SchemaGenerator();
25+
var schema = generator.GetSchema<IMyService>();
26+
Log(schema);
27+
Assert.Equal(@"syntax = ""proto3"";
28+
package protobuf_net.Grpc.Reflection.Test;
29+
import ""google/protobuf/timestamp.proto"";
30+
import ""google/protobuf/empty.proto"";
31+
32+
enum Category {
33+
Default = 0;
34+
Foo = 1;
35+
Bar = 2;
36+
}
37+
message MyRequest {
38+
int32 Id = 1;
39+
.google.protobuf.Timestamp When = 2;
40+
}
41+
message MyResponse {
42+
string Value = 1;
43+
Category Category = 2;
44+
string RefId = 3; // default value could not be applied: 00000000-0000-0000-0000-000000000000
45+
}
46+
service MyService {
47+
rpc AsyncEmpty (.google.protobuf.Empty) returns (.google.protobuf.Empty);
48+
rpc ClientStreaming (MyResponse) returns (stream MyRequest);
49+
rpc FullDuplex (stream MyResponse) returns (stream MyRequest);
50+
rpc ServerStreaming (stream MyResponse) returns (MyRequest);
51+
rpc SyncEmpty (.google.protobuf.Empty) returns (.google.protobuf.Empty);
52+
rpc Unary (MyResponse) returns (MyRequest);
53+
}
54+
", schema, ignoreLineEndingDifferences: true);
55+
}
56+
57+
[ServiceContract]
58+
public interface IMyService
59+
{
60+
ValueTask<MyResponse> Unary(MyRequest request, CallContext callContext = default);
61+
ValueTask<MyResponse> ClientStreaming(IAsyncEnumerable<MyRequest> request, CallContext callContext = default);
62+
IAsyncEnumerable<MyResponse> ServerStreaming(MyRequest request, CallContext callContext = default);
63+
IAsyncEnumerable<MyResponse> FullDuplex(IAsyncEnumerable<MyRequest> request, CallContext callContext = default);
64+
65+
ValueTask AsyncEmpty();
66+
void SyncEmpty();
67+
}
68+
69+
[DataContract]
70+
public class MyRequest
71+
{
72+
[DataMember(Order = 1)]
73+
public int Id { get; set; }
74+
75+
// with protobuf-net v3, this could use DataContract/DataMember throughput, and
76+
// just use CompatibilityLevel 300+
77+
[DataMember(Order = 2)]
78+
public DateTime When { get; set; }
79+
}
80+
81+
[DataContract]
82+
public class MyResponse
83+
{
84+
[DataMember(Order = 1)]
85+
public string? Value { get; set; }
86+
87+
[DataMember(Order = 2)]
88+
public Category Category {get;set;}
89+
90+
[DataMember(Order = 3)]
91+
public Guid RefId { get; set; }
92+
}
93+
94+
public enum Category
95+
{
96+
Default = 0,
97+
Foo = 1,
98+
Bar = 2,
99+
}
100+
}
101+
}

tests/protobuf-net.Grpc.Test.IntegrationUpLevel/protobuf-net.Grpc.Test.IntegrationUpLevel.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
14-
<PackageReference Include="protobuf-net" Version="3.0.2" />
14+
<PackageReference Include="protobuf-net" Version="3.0.21" />
1515
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />
1616
<PackageReference Include="xunit" Version="2.4.1" />
1717
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">

tests/protobuf-net.Grpc.Test/ContractOperationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void ServerSignatureCount()
6262
}
6363

6464
[Fact]
65-
public void CheckAllMethodsConvered()
65+
public void CheckAllMethodsCovered()
6666
{
6767
var expected = new HashSet<string>(typeof(IAllOptions).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Select(x => x.Name));
6868
Assert.Equal(ContractOperation.SignatureCount, expected.Count);

0 commit comments

Comments
 (0)