In this post, we’ll create a new gRPC project in ASP.NET Core and see what’s inside of them.
Why not make an introduction on what is gRPC? Well, there are several introduction posts on gRPC in .NET so I don’t want to write another introduction to the gRPC framework again.
I’ve found some best introduction posts on gRPC in ASP.NET core
Here are some advantages of using gRPC
- Lightweight RPC framework, which is language agnostic and works in any environment
- Supports bi-directional streaming
- gRPC by default uses Protobuf binary serialization, which reduces network usage
Now, Let’s create a new gRPC application in Visual Studio.
Prerequisites
- Visual Studio 2022 with ASP.NET and web development workload installed.
Creating a gRPC Server
To create a gRPC server, we have a new template in Visual Studio 2022.
- Start Visual Studio 2022 and select Create New project
- Search for “grpc” in the search box you should see ASP.NET Core gRPC Service. Select it
- Give it a name and in the next screen select .NET 6 and select Create
That should create a brand new gRPC Server application.
Exploring newly created gRPC project
Let’s look at what we have in our solution.
Our newly created solution has a Protos
folder with a greet.proto
file and a Services
folder with GreeterService.cs
and a Program.cs
file.
Let’s see what we have in each of them.
public class GreeterService : Greeter.GreeterBase { private readonly ILogger<GreeterService> _logger; public GreeterService(ILogger<GreeterService> logger) { _logger = logger; } public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } }
The GreeterService
is implemented from Greet.GreeterBase
. And we have one method SayHello
which overrides the one in the GreeterBase class (we will revisit this class when we see the compiler-generated code later in this blog post).
The SayHello
method just wraps the name it got in the request and prepends with Hello and sends it as a response.
Where are the message types declared?
We see HelloReply and HelloRequest types in the GreeterService.cs. Let’s understand what happens.
When we declare a message type in the greet.proto
file and save the file in visual studio, the IDE reads the greet.proto
file on the fly to generate the required types in a Greet.cs
file in <project-folder>\obj\Debug\net6.0\Protos\
folder.
The greet.proto
file has HelloReply
and HelloRequest
types declared and they are not C# classes so we cannot see them in the solution explorer.
Note: You need not worry about the compiler-generated C# code if you are not using Visual Studio 2022. During the MS Build process, we should get those compiler-generated types.
How does Visual Studio recognize them as types from the proto file?
Let’s quickly search for our HelloReply
in Visual Studio 2022.
So, HelloReply and HelloRequest are C# classes under the hood which you won’t see them in solution explorer.
Understanding .proto file
Let’s have a look at our proto file.
syntax = "proto3"; option csharp_namespace = "GrpcTester"; package greet; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply); } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings. message HelloReply { string message = 1; }
The first line in every .proto file should be the syntax of the proto file. If we don’t do this, the protocol buffer compiler will assume we are using proto2
syntax.
The greet.proto file has an RPC service called Greeter
and a method SayHello
, which takes in HelloRequest
and returns a HelloReply
.
And it also has two message types HelloRequest
and HelloReply
which has name and message properties on it.
Notice how the message type properties are marked with 1 as the value. These are called Field Numbers. If we had more properties on a message type, we’d increment the number count. Like for example
message SearchRequest { string query = 1; int32 page = 2; int32 results_per_page = 3; }
Why do we need to assign a number to the fields on the message types?
These field numbers have to be unique for each field in the message definition. Because during the serialization process, these filed numbers are the identifiers for the messages in binary format.
For example, if we have a message like this
message Test { optional int32 a = 1; }
And we create a Test message and set the value of a
to 150, and we then serialize this message to an output stream, the encoded message will have 3 bytes:
08 96 01
If we use a protobuf decoder and decode it the result will be 1:150
. Here we see field number 1 representing the value 150 (which is our message).
What’s in our compiled C# files from .proto file?
We had 2 message types (HelloRequest
, HelloReply
) and an RPC service Greeter
. Let’s look at what we have in our Protos folder inside obj/debug/net6.0
folder.
We have 2 csharp files. Greet.cs
and GreetGrpc.cs
. The Greet.cs
file holds the message type definitions and GreetGrpc.cs
has a server-side implementation of the Greeter, which is the SayHello
method declared as part of the service in the proto file.
Here are the Greet and GreetGrpc files.
// <auto-generated> // Generated by the protocol buffer compiler. DO NOT EDIT! // source: Protos/greet.proto // </auto-generated> #pragma warning disable 1591, 0612, 3021 #region Designer generated code using pb = global::Google.Protobuf; using pbc = global::Google.Protobuf.Collections; using pbr = global::Google.Protobuf.Reflection; using scg = global::System.Collections.Generic; namespace GrpcTester { /// <summary>Holder for reflection information generated from Protos/greet.proto</summary> public static partial class GreetReflection { #region Descriptor /// <summary>File descriptor for Protos/greet.proto</summary> public static pbr::FileDescriptor Descriptor { get { return descriptor; } } private static pbr::FileDescriptor descriptor; static GreetReflection() { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "ChJQcm90b3MvZ3JlZXQucHJvdG8SBWdyZWV0IhwKDEhlbGxvUmVxdWVzdBIM", "CgRuYW1lGAEgASgJIh0KCkhlbGxvUmVwbHkSDwoHbWVzc2FnZRgBIAEoCTI9", "CgdHcmVldGVyEjIKCFNheUhlbGxvEhMuZ3JlZXQuSGVsbG9SZXF1ZXN0GhEu", "Z3JlZXQuSGVsbG9SZXBseUINqgIKR3JwY1Rlc3RlcmIGcHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::GrpcTester.HelloRequest), global::GrpcTester.HelloRequest.Parser, new[]{ "Name" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::GrpcTester.HelloReply), global::GrpcTester.HelloReply.Parser, new[]{ "Message" }, null, null, null, null) })); } #endregion } #region Messages /// <summary> /// The request message containing the user's name. /// </summary> public sealed partial class HelloRequest : pb::IMessage<HelloRequest> #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { private static readonly pb::MessageParser<HelloRequest> _parser = new pb::MessageParser<HelloRequest>(() => new HelloRequest()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public static pb::MessageParser<HelloRequest> Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public static pbr::MessageDescriptor Descriptor { get { return global::GrpcTester.GreetReflection.Descriptor.MessageTypes[0]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] pbr::MessageDescriptor pb::IMessage.Descriptor { get { return Descriptor; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloRequest() { OnConstruction(); } partial void OnConstruction(); [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloRequest(HelloRequest other) : this() { name_ = other.name_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloRequest Clone() { return new HelloRequest(this); } /// <summary>Field number for the "name" field.</summary> public const int NameFieldNumber = 1; private string name_ = ""; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public string Name { get { return name_; } set { name_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override bool Equals(object other) { return Equals(other as HelloRequest); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public bool Equals(HelloRequest other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } if (Name != other.Name) return false; return Equals(_unknownFields, other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override int GetHashCode() { int hash = 1; if (Name.Length != 0) hash ^= Name.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } return hash; } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override string ToString() { return pb::JsonFormatter.ToDiagnosticString(this); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else if (Name.Length != 0) { output.WriteRawTag(10); output.WriteString(Name); } if (_unknownFields != null) { _unknownFields.WriteTo(output); } #endif } #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE [global::System.Diagnostics.DebuggerNonUserCodeAttribute] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { if (Name.Length != 0) { output.WriteRawTag(10); output.WriteString(Name); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } } #endif [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public int CalculateSize() { int size = 0; if (Name.Length != 0) { size += 1 + pb::CodedOutputStream.ComputeStringSize(Name); } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } return size; } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void MergeFrom(HelloRequest other) { if (other == null) { return; } if (other.Name.Length != 0) { Name = other.Name; } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void MergeFrom(pb::CodedInputStream input) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE input.ReadRawMessage(this); #else uint tag; while ((tag = input.ReadTag()) != 0) { switch(tag) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { Name = input.ReadString(); break; } } } #endif } #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE [global::System.Diagnostics.DebuggerNonUserCodeAttribute] void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { uint tag; while ((tag = input.ReadTag()) != 0) { switch(tag) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { Name = input.ReadString(); break; } } } } #endif } /// <summary> /// The response message containing the greetings. /// </summary> public sealed partial class HelloReply : pb::IMessage<HelloReply> #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { private static readonly pb::MessageParser<HelloReply> _parser = new pb::MessageParser<HelloReply>(() => new HelloReply()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public static pb::MessageParser<HelloReply> Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public static pbr::MessageDescriptor Descriptor { get { return global::GrpcTester.GreetReflection.Descriptor.MessageTypes[1]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] pbr::MessageDescriptor pb::IMessage.Descriptor { get { return Descriptor; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloReply() { OnConstruction(); } partial void OnConstruction(); [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloReply(HelloReply other) : this() { message_ = other.message_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public HelloReply Clone() { return new HelloReply(this); } /// <summary>Field number for the "message" field.</summary> public const int MessageFieldNumber = 1; private string message_ = ""; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public string Message { get { return message_; } set { message_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override bool Equals(object other) { return Equals(other as HelloReply); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public bool Equals(HelloReply other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } if (Message != other.Message) return false; return Equals(_unknownFields, other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override int GetHashCode() { int hash = 1; if (Message.Length != 0) hash ^= Message.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } return hash; } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public override string ToString() { return pb::JsonFormatter.ToDiagnosticString(this); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else if (Message.Length != 0) { output.WriteRawTag(10); output.WriteString(Message); } if (_unknownFields != null) { _unknownFields.WriteTo(output); } #endif } #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE [global::System.Diagnostics.DebuggerNonUserCodeAttribute] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { if (Message.Length != 0) { output.WriteRawTag(10); output.WriteString(Message); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } } #endif [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public int CalculateSize() { int size = 0; if (Message.Length != 0) { size += 1 + pb::CodedOutputStream.ComputeStringSize(Message); } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } return size; } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void MergeFrom(HelloReply other) { if (other == null) { return; } if (other.Message.Length != 0) { Message = other.Message; } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] public void MergeFrom(pb::CodedInputStream input) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE input.ReadRawMessage(this); #else uint tag; while ((tag = input.ReadTag()) != 0) { switch(tag) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { Message = input.ReadString(); break; } } } #endif } #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE [global::System.Diagnostics.DebuggerNonUserCodeAttribute] void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { uint tag; while ((tag = input.ReadTag()) != 0) { switch(tag) { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { Message = input.ReadString(); break; } } } } #endif } #endregion } #endregion Designer generated code
I know it’s hard to read the Greet.cs file due to boilerplate code.
The Greet.cs file has 3 classes
- GreetReflection: Holds the reflection information generated from .proto file
- HelloRequest: A partial sealed class that defines our Name property and other boilerplate implementations as HelloRequest inherits from IMessage.
- HelloReply: A partial sealed class that defines our Message property and other boilerplate implementations as HelloReply inherits from IMessage.
And this is the GreetGrpc.cs
file.
// <auto-generated> // Generated by the protocol buffer compiler. DO NOT EDIT! // source: Protos/greet.proto // </auto-generated> #pragma warning disable 0414, 1591 #region Designer generated code using grpc = global::Grpc.Core; namespace GrpcTester { /// <summary> /// The greeting service definition. /// </summary> public static partial class Greeter { static readonly string __ServiceName = "greet.Greeter"; [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context) { #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION if (message is global::Google.Protobuf.IBufferMessage) { context.SetPayloadLength(message.CalculateSize()); global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter()); context.Complete(); return; } #endif context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message)); } [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static class __Helper_MessageCache<T> { public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T)); } [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T> { #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION if (__Helper_MessageCache<T>.IsBufferMessage) { return parser.ParseFrom(context.PayloadAsReadOnlySequence()); } #endif return parser.ParseFrom(context.PayloadAsNewBuffer()); } [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static readonly grpc::Marshaller<global::GrpcTester.HelloRequest> __Marshaller_greet_HelloRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::GrpcTester.HelloRequest.Parser)); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static readonly grpc::Marshaller<global::GrpcTester.HelloReply> __Marshaller_greet_HelloReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::GrpcTester.HelloReply.Parser)); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static readonly grpc::Method<global::GrpcTester.HelloRequest, global::GrpcTester.HelloReply> __Method_SayHello = new grpc::Method<global::GrpcTester.HelloRequest, global::GrpcTester.HelloReply>( grpc::MethodType.Unary, __ServiceName, "SayHello", __Marshaller_greet_HelloRequest, __Marshaller_greet_HelloReply); /// <summary>Service descriptor</summary> public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor { get { return global::GrpcTester.GreetReflection.Descriptor.Services[0]; } } /// <summary>Base class for server-side implementations of Greeter</summary> [grpc::BindServiceMethod(typeof(Greeter), "BindService")] public abstract partial class GreeterBase { /// <summary> /// Sends a greeting /// </summary> /// <param name="request">The request received from the client.</param> /// <param name="context">The context of the server-side call handler being invoked.</param> /// <returns>The response to send back to the client (wrapped by a task).</returns> [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] public virtual global::System.Threading.Tasks.Task<global::GrpcTester.HelloReply> SayHello(global::GrpcTester.HelloRequest request, grpc::ServerCallContext context) { throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); } } /// <summary>Creates service definition that can be registered with a server</summary> /// <param name="serviceImpl">An object implementing the server-side handling logic.</param> [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] public static grpc::ServerServiceDefinition BindService(GreeterBase serviceImpl) { return grpc::ServerServiceDefinition.CreateBuilder() .AddMethod(__Method_SayHello, serviceImpl.SayHello).Build(); } /// <summary>Register service method with a service binder with or without implementation. Useful when customizing the service binding logic. /// Note: this method is part of an experimental API that can change or be removed without any prior notice.</summary> /// <param name="serviceBinder">Service methods will be bound by calling <c>AddMethod</c> on this object.</param> /// <param name="serviceImpl">An object implementing the server-side handling logic.</param> [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] public static void BindService(grpc::ServiceBinderBase serviceBinder, GreeterBase serviceImpl) { serviceBinder.AddMethod(__Method_SayHello, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::GrpcTester.HelloRequest, global::GrpcTester.HelloReply>(serviceImpl.SayHello)); } } } #endregion
We have two static methods for serializing and deserializing the messages (__Helper_SerializeMessage
, __Helper_DeserializeMessage
) inside Greeter static class.
And we had an abstract partial class (GreeterBase
) having a virtual method SayHello
. Remember we had the following method in our GreeterService class (earlier in the post), which overwrites the SayHello
method in the abstract class.
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); }
Running the Grpc Service
Just like any other project in Visual Studio, just hit F5 or use dotnet run
command.
Here is the output when running the gRPC service.
But, you won’t be able to test the gRPC service directly in a browser, you need a gRPC client to test it because the client should also support gRPC to make a request for the gRPC service.
If we try to hit one of those exposed endpoints, we’d see this.
Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909
This is because the Program.cs
is set up with the default route.
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
References
Karthik is a passionate Full Stack developer working primarily on .NET Core, microservices, distributed systems, VUE and JavaScript. He also loves NBA basketball so you might find some NBA examples in his posts and he owns this blog.
Pingback: Dew Drop – April 17, 2023 (#3923) – Morning Dew by Alvin Ashcraft