Sinan
Sinan

Reputation: 908

Generate .proto file of generic services with protobuf-net.Grpc

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

Answers (2)

Sinan
Sinan

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

Marc Gravell
Marc Gravell

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

Related Questions