Reputation: 908
I am trying to generate the .proto of this structure:
-- MODELS --
base model
[DataContract]
public abstract class Base
{
[ProtoMember(1)]
public string Id { get; set; }
[ProtoMember(2, DataFormat = DataFormat.WellKnown)]
public DateTime CreatedDate { get; private set; } = DateTime.UtcNow;
[ProtoMember(3, DataFormat = DataFormat.WellKnown)]
public DateTime UpdatedDate { get; set; } = DateTime.UtcNow;
}
Todo model
[ProtoContract]
public class Todo : Base
{
[ProtoMember(1)]
public string Title { get; set; }
[ProtoMember(2)]
public string Content { get; set; }
[ProtoMember(3)]
public string Category { get; set; }
}
Plus this line:
RuntimeTypeModel.Default[typeof(Base)].AddSubType(42, typeof(Todo));
-- CONTRACTS --
Base contract
[ServiceContract]
public interface IBaseService<T>
{
// CREATE
[OperationContract]
Task<RStatus> CreateOneAsync(T request,CallContext context = default);
// FIND
[OperationContract]
ValueTask<T> GetById(UniqueIdentification request,CallContext context = default);
}
Todo contract
[ServiceContract]
public interface ITodoService : IBaseService<Todo>
{
// FIND
[OperationContract]
ValueTask<Todo> GetOneByQueryAsync(Query query, CallContext context = default);
}
With this generic approach, I am trying to prevent repeating code.
-- Startup.cs --
...
endpoints.MapGrpcService<TodoService>();
endpoints.MapCodeFirstGrpcReflectionService();
...
So, when I run this :
var schema = generator.GetSchema<ITodoService>();
I get this output in the .proto file:
syntax = "proto3";
package Nnet.Contracts;
import "google/protobuf/timestamp.proto";
message Base {
string Id = 1;
.google.protobuf.Timestamp CreatedDate = 2;
.google.protobuf.Timestamp UpdatedDate = 3;
oneof subtype {
Todo Todo = 42;
}
}
message IEnumerable_Todo {
repeated Base items = 1;
}
message Query {
string Filter = 1;
}
message Todo {
string Title = 1;
string Content = 2;
string Category = 3;
}
service TodoService {
rpc GetOneByQuery (Query) returns (Base);
}
In the .proto file section service Todoservice
, I am missing the other two functions from the Base contract. Also, the return type of the function rpc GetOneByQuery (Query) returns (Base);
is wrong, it should be Todo.
Any suggestions?
Upvotes: 2
Views: 1661
Reputation: 908
Well, for now all my services are in C#, but it will be great to support this schema of interface inheritance for the future in case we have to share the .proto files to others than c# applications. I am not sure if it's alright to paste the code that may help you to support this feature... So, this is the code:
I am skipping the models
-- CONTRACTS --
//Base Contract
public interface IBaseService<T>
{
Task<RStatus> CreateOne(T request);
ValueTask<T> GetById(UniqueIdentification request);
}
//Product Contract
public interface IProductService<T> : IBaseService<T>
{
Task<T> GetByBrandName(string request);
ValueTask<T> GetByName(string request);
}
//Audio Contract
public interface IAudioService<T>
{
Task<T> GetBySoundQuality(int request);
}
//Headphones Contract
public interface IHeadphonesService : IProductService<Headphones>, IAudioService<Headphones>
{
Task<Headphones> GetByBluetoothOption(bool request);
}
-- PROGRAM.CS --
static void Main(string[] args)
{
foreach (var type in TypesToGenerateForType(typeof(IHeadphonesService)))
{
Console.WriteLine($"Type: {type} \n");
}
}
public static IEnumerable<Type> TypesToGenerateForType(Type type)
{
foreach (var interfaceType in type.FindInterfaces((ignored, data) => true, null))
{
foreach (var dm in interfaceType.GetMethods())
{
Console.WriteLine($"Method Name: {dm}");
}
yield return interfaceType;
}
foreach (var tm in type.GetMethods())
{
Console.WriteLine($"Method Name: {tm}");
}
yield return type;
}
The output:
Method Name: System.Threading.Tasks.Task`1[TestIntInh.Shared.Models.Headphones] GetByBrandName(System.String)
Method Name: System.Threading.Tasks.ValueTask`1[TestIntInh.Shared.Models.Headphones] GetByName(System.String)
Type: TestIntInh.Shared.Contracts.IProductService`1[TestIntInh.Shared.Models.Headphones
Method Name: System.Threading.Tasks.Task`1[TestIntInh.Shared.Models.RStatus] CreateOne(TestIntInh.Shared.Models.Headphones)
Method Name: System.Threading.Tasks.ValueTask`1[TestIntInh.Shared.Models.Headphones] GetById(TestIntInh.Shared.Models.UniqueIdentification)
Type: TestIntInh.Shared.Contracts.IBaseService`1[TestIntInh.Shared.Models.Headphones]
Method Name: System.Threading.Tasks.Task`1[TestIntInh.Shared.Models.Headphones] GetBySoundQuality(Int32)
Type: TestIntInh.Shared.Contracts.IAudioService`1[TestIntInh.Shared.Models.Headphones]
Method Name: System.Threading.Tasks.Task`1[TestIntInh.Shared.Models.Headphones] GetByBluetoothOption(Boolean)
Type: TestIntInh.Shared.Contracts.IHeadphonesService
I think with this you can build the appropriate .proto file. The FindInterfaces
gives you pretty much all you might need.
Upvotes: 0
Reputation: 1062492
Also, the return type of the function rpc GetOneByQuery (Query) returns (Base); is wrong, it should be Todo.
No, that's correct; protobuf itself has no concept of inheritance - protobuf-net has to shim it in, which it does using encapsulation, hence the Base
with a oneof subtype
that has a Todo
. In your case, we expect that the thing passed will always actually resolve as a Todo
, but the .proto schema language has no syntax to express that. The absolute best we could do here would be to include an extra comment in the generated .proto saying // return type will always be a Todo
or similar.
I am missing the other two functions from the Base contract
service inheritance and generic services are not currently well supported here; again, these are concepts that have no matching metaphor in .proto or gRPC in general, and protobuf-net would need to invent something suitable; I have not - to date - sat down and thought through any such scheme or the implications there-of. Fundamentally, the problem here is that the service contract and name are used to construct a route/url; when talking about a single service contract and method, that's fine - but when talking about service inheritance and generics, it gets a lot more complicated to uniquely identify what service you're talking about, and how that should map between a route and an implementation (and, indeed, the .proto syntax). I'm entirely open to thoughts here - it just hasn't been a critical-path requirement to date.
Upvotes: 1