Code-first gRPC

Code-first gRPC or how to add gRPC to an existing app #

There are cases when we are not lucky enough to start green-field projects, and we are not able to add the technologies we want or need from the project inception. In these cases, when it comes to using gRPC we can still retrofit it by leveraging an awesome library build by Mark Gravell.

Exposing code-first gRPC data types #

To add gRPC support for an existing app we would need to install protobuf-net.Grpc package and to expose the types and operations by annotating them with some attributes. Below there are two types that we will use to be our request/response types. These are annotated with [DataContract], and [DataMember(Order = 1)].

    [DataContract]
    public class PersonRequest
    {
        [DataMember(Order = 1)]
        public string Name { get; set; }
    }

    [DataContract]
    public class PersonResponse
    {
        [DataMember(Order = 1)]
        public string Message { get; set; }
    }

Once we define the types that we will use, we would need to define an interface that exposes our operations. In our case, we will have only one operation that will behave like a classic request/response. And by that I mean that is not streaming from client or server-side.

[ServiceContract]
public interface IPersonContract
{
    Task<PersonResponse> SayHi(PersonRequest request, CallContext context = default);
}

This interface is our contract, and what we want to expose to consumers. It is up to you how you pack these types, either in the same gRPC project, or as a separate class library. I choose to have a separate class library because it is easier to share between different projects. Another reason is that I didn't want to reference the gRPC service directly in the consumer.

The next step is to take this interface and actually provide an implementation for the operations listed. This project is a regular gRPC service template, in which we install the protobuf-net.Grpc.AspNetCore NuGet package and enable the corresponding middleware builder.Services.AddCodeFirstGrpc(); in Program.cs.

You will notice that unlike using gRPC in a new project, in here we won't have code that is generated for us, but we will this interface that will be the 'definition' for our gRPC service. So when we give the actual implementation, we will implement what is specified in the interface, and not extend a generated base class as before.


 public class PersonService : IPersonContract
 {
   public Task<PersonResponse> SayHi(PersonRequest request, CallContext context = default)
   {
         return Task.FromResult(
              new PersonResponse
              {
                  Message = $"Hello {request.Name}"
              });
   }
 }

Consuming the code-first gRPC service #

Once we provide an actual implementation we can create a project that consumes our gRPC service. In this case I will use a console app and install 2 NuGet packages: protobuf-net.Grpc and Grpc.Net.Client. The first one allows us to work with gRPC code-first approach. The second allows us to call to call gRPC services.

After package install step we need to make sure we have access to the interface that exposes the types and operations. Depending on how you organized your code you need add a project reference. In my case I have a separate project that holds these types, and I needed to add the reference to the SharedContracts class library. With all being done, now we can call the gRPC service.

using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;
using SharedContracts;

Console.WriteLine("Hello, World!");
using var channel = GrpcChannel.ForAddress("https://localhost:7251");
var client = channel.CreateGrpcService<IPersonContract>();

var reply = await client.Unary(
    new PersonRequest { Name = "Gigi Dev" });

Console.WriteLine($"Greeting: {reply.Message}");

As you notice, we still need a channel that points to the gRPC service location, but we don't use the base client that is generated for us by the protoc compiler, we obtain a service instance ourselves by calling .CreateGrpcService<IPersonContract>();.

Another difference will be that we don't have a .proto file in our application that is shared between the service and the consumer of our service. Instead, we share C# code, by referencing it inside our consumer.

Summary #

If you want to add gRPC to an existing project, let's say, a WCF one, you can do it without writing from scratch a .proto file that replicates your existing types and operations. This save some precious time and still gives you the perks of working with gRPC. The downside is that you won't have the interoperability you get when you work with Protocol Buffers. Those .proto files are agnostic and can be interpreted by different programming languages, giving you some flexibility. When you use the Code-First gRPC approach, you won't have that, since you are sharing C# code between your projects.


PS: If you want to actually execute the code I used, fill this form and you will receive the zip file directly in your inbox.