With WCF (Windows Communication Foundation) no longer being actively developed, gRPC (remote procedure call) appears to be the natural replacement when it comes to developing greenfield service applications on .NET Core and .NET 5. Unlike ASP.NET Core Web APIs, it supports both bidirectional streaming over HTTP/2 and has a small and efficient default binary message format, which makes it an ideal choice for implementing low latency and high throughput real-time services.

gRPC was originally developed and used internally by Google (hence the little g) and is now fully open sourced on GitHub (http://bit.ly/2YZRNOJ). It's being described as “a modern open source high performance RPC framework that can run in any environment” and includes code generators that produce client and server-side stubs for a variety of programming languages, including C#.

Bindings

In WCF, you define one or several endpoints to expose your services. Each endpoint consists of an address, a binding, and a contract. The address tells where to find the service and the binding defines the transport protocol to use when communicating with it, how to encode the messages, and what security mechanisms to use. You can choose among a number of system-provided bindings or create your own custom ones.

In the .NET implementations of gRPC, the communication is done over HTTP/2. No other transport protocols are supported. On top of the transport layer, there's the concept of a channel. Under the hood, the channel takes care of connecting to the server and handles things such as load balancing and connection pooling. It provides a single virtual connection, which may be backed by several physical connections internally, to a conceptual endpoint. As an application developer, you don't really need to bother with the details of how this is implemented. You write your code against the generated stubs rather than dealing with the channel directly.

Contracts

The stubs are generated based on a .proto file, which is nothing but a plain text file with a .proto file extension. It defines the service contract and the structure of the payload messages to be exchanged between the client and the server. In WCF, there's a concept of a Web service description language (WSDL) and a metadata exchange (MEX) endpoint that both describe the metadata of a service. gRPC supports a server reflection protocol for the same purpose, but the common and preferred way to expose a service's methods and how to call them is to simply share the .proto file with the consuming clients.

The serialization format in gRPC is agnostic, but the default language interface definition language (IDL) that's used to describe the service and the payload messages is called the protocol buffer language. A protocol compiler, called protoc, is used to compile the .proto file and generate code in any of the supported programming languages. For C#, it generates a .cs source file with a class for each message type that's defined in the file and another source file that contains the base classes for the gRPC service and client.

Let's take a quick look at how to migrate the following WCF service from the classic getting started sample in the official docs to gRPC:

[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")
public interface ICalculator
{
    [OperationContract]
    double Add(double n1, double n2);
    
    [OperationContract]
    double Subtract(double n1, double n2);
    
    [OperationContract]
    double Multiply(double n1, double n2);
    
    [OperationContract]
    double Divide(double n1, double n2);
}

The first step is to create a .proto file. I prefer to put it in a .NET Standard class library that can be referenced from both client and server applications. The contents of the file might look something like this:

syntax = "proto3";
option csharp_namespace = "SharedLib.Generated";
service CalculatorService {
  rpc Add (CalculatorRequest) returns (CalculatorReply) {}
  rpc Subtract (CalculatorRequest) returns (CalculatorReply) {}
  rpc Multiply (CalculatorRequest) returns (CalculatorReply) {}
  rpc Divide (CalculatorRequest) returns (CalculatorReply) {}
}

message CalculatorRequest {
  double n1 = 1;
  double n2 = 2;
}

message CalculatorReply {
  double result = 1;
}

Note that each method accepts only a single payload message, similar to a WCF message contract. You then define fields with unique numbers in each message. The numbers are required to be able to encode and decode binary messages over the wire without breaking backward compatibility. These binary messages are known as protocol buffers (or Protobuf for short) - a fast language and platform-neutral serialization mechanism that was also developed by Google. Unlike both XML and JSON, the messages are encoded in a binary format, which makes them small and easy to write and fast to read. According to the docs, protocol buffers are, for example, three to 10 times smaller and 20 to 100 times faster than using XML for serializing structured data. You'll find more information about how to write .proto files, including a list of all support data types, in the protocol buffers language guide at https://bit.ly/2Z5bl8x.

Code Generation

There are two different sets of libraries that provide gRPC support in .NET. Grpc.Core is Google's original implementation. It's a managed wrapper that invokes the functionality of a native C library via P/Invoke. grpc-dotnet is Microsoft's new implementation of gRPC in ASP.NET Core 3. It uses the cross-platform Kestrel Web server on the server-side and the System.Net.HttpClient class on the client-side and doesn't rely on any native library. Unlike Grpc.Core, it targets .NET Standard 2.1 and isn't compatible with the .NET Framework or any previous version of .NET Core.

If you have any server or client applications to be migrated to gRPC but are yet to be upgraded to .NET Core 3, you should install the required Grpc and Google.Protobuf NuGet packages and optionally also the Grpc.Tools package into your class library project after you've created the .proto file. Grpc.Tools is a development dependency that integrates with MSBuild to provide automatic code generation of the .proto file as part of the build process. In order for any code to be generated, you also need to add a <Protobuf> element to your project file where you reference the .proto file. The complete contents of the .csproj file should look something like Listing 1. Grpc is a metapackage for Grpc.Core.

Listing 1: The project file with all required packages

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard1.5</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.9.1" />
    <PackageReference Include="Grpc" Version="2.23.0" />
    <PackageReference Include="Grpc.Tools" Version="2.23.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native;contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="calculator.proto" />
  </ItemGroup>
</Project>

If you use grpc-dotnet and .NET Core 3, there's a new dotnet-grpc tool that installs all required packages and modifies the project file for you. You can use it from the command-line or by choosing Project > Add Service Reference in Visual Studio 2019 16.3 and selecting the “Add new gRPC service reference” option, as shown in Figure 1.

Figure 1: The gRPC service reference dialog in Visual Studio
Figure 1: The gRPC service reference dialog in Visual Studio

If you build the project and look in the obj/Debug/netstandard2.x folder (replace Debug with Release or whatever build configuration you may be using) inside the project directory, you should see two generated files named Calculator.cs and CalculatorGrpc.cs. The former contains the CalculatorRequest and CalculatorReply message types and the latter contains the stubs for the gRPC service and client. If you prefer to include these source files in your project, you can add the following attributes to the <Protobuf> element in the .csproj file. You shouldn't edit the source files manually though.

<Protobuf Include="calculator.proto"
          OutputDir="%(RelativePath)"
          CompileOutputs="false" />

Services

The next step is to implement the service methods. Create another class library that targets the same version of .NET Standard as the previously created library where the .proto file and the generated code files are located and add a reference to this project. Then add a class that extends the abstract CalculatorService.CalculatorServiceBase class in CalculatorGrpc.cs. Unlike in WCF, there's no service interface to be implemented in gRPC. Instead, code generation using protoc and overriding methods of the generated base class is the standard practice. In this case, the actual implementation looks something like Listing 2.

Listing 2: The service implementation

public class CalculatorService : CalculatorServiceBase
{
    public override Task<CalculatorReply> Add(CalculatorRequest request, ServerCallContext context) => 
        Task.FromResult(new CalculatorReply() { Result = request.N1 + request.N2 });

    public override Task<CalculatorReply> Subtract(CalculatorRequest request, ServerCallContext context) =>
        Task.FromResult(new CalculatorReply() { Result = request.N1 - request.N2 });

    public override Task<CalculatorReply> Multiply(CalculatorRequest request, ServerCallContext context) =>
        Task.FromResult(new CalculatorReply() { Result = request.N1 * request.N2 });

    public override Task<CalculatorReply> Divide(CalculatorRequest request, ServerCallContext context)
    {
        if (request.N2 == 0)
            throw new RpcException(new Status(StatusCode.InvalidArgument, "Divide by zero attempt."));

        return Task.FromResult(new CalculatorReply() {Result = request.N1 / request.N2 });
    }
}

If you look at the virtual methods in the base class, you'll see that they throw an exception by default. This is, for example, how the Add method is implemented in CalculatorServiceBase:

public virtual Task<CalculatorReply> Add(CalculatorRequest request, ServerCallContext context)
{
    throw new RpcException(new Status(StatusCode.Unimplemented, ""));
}

The compiler won't force you to provide an implementation for all methods as it does when you implement an interface. So if you forget to override any method that's defined in the .proto file, a client will get a runtime exception when trying to call this particular method.

Also note that the method signatures are asynchronous, and all methods must return a Task or Task<T>. There's no support or configuration setting for synchronous implementations or overloads of service methods in the C# variants of gRPC. If you don't use the await operator anywhere in the method, you can simply wrap the synchronously computed return value in a completed Task using the Task.FromResult method, as shown in Listing 2.

Hosts

Once you have implemented the service, you need to host it in a process. Hosting a Grpc.Core service in a managed process is very similar to hosting a WCF service. Instead of creating a ServiceHost to which you add endpoints, you create a Grpc.Core.Server to which you add services and ports, like in Listing 3. A single server process may host several service implementations and listen for requests on several different ports.

Listing 3: The Grpc.Core service host

class Program
{
    const string Host = "localhost";
    const int Port = 50051;

    static async Task Main(string[] args)
    {
        Server server = new Server();
        server.Services.Add(CalculatorService.BindService(ServiceLib.CalculatorService()));
        server.Ports.Add(new ServerPort(Host, Port, ServerCredentials.Insecure));
        server.Start();

        Console.WriteLine($"The service is ready at {Host} on port {Port}.");
        Console.WriteLine("Press any key to stop the service...");
        Console.ReadLine();

        await server.ShutdownAsync();
    }
}

Grpc-dotnet provides a new gRPC Service project template that you'll find under File > New > Project in the latest version of Visual Studio. Once you've created the project, you can remove the Protos and Services folders that contain some default files and add a reference to your service library where the CalculatorService class is implemented. If you then replace the reference to the removed GreeterService with your service type in the Configure method of the Startup class, you should be able to build and run the app:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseRouting();
    app.UseEndpoints(endpoints => endpoints.MapGrpcService<CalculatorService>());
}

Besides the managed implementation, one of the benefits of using grpc-dotnet is that you get the built-in support for dependency injection, configuration and logging that ASP.NET Core provides.

Clients

Next, you need a client. Create another Console App (.NET Core) project and add a reference to the class library where the .proto file is located. You can then connect to the server by creating an instance of the generated CalculatorServiceClient class and pass a Grpc.Core.ChannelBase to its constructor, as shown in Listing 4. ChannelBase is an abstract class that Grpc.Core provides a concrete Channel implementation of. For grpc-dotnet, there's a fully managed client API that you can use by installing the Grpc.Net.Client NuGet package into your client console app. It provides a static factory method called GrpcChannel.ForAddress that accepts an address and creates a HttpClient under the hood. Listing 5 contains an example of how to use it to call the CalculatorService.

Listing 4: The Grpc.Core client

class Program
{
    static async Task Main(string[] args)
    {
        Channel channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
        CalculatorService.CalculatorServiceClient client = new CalculatorService.CalculatorServiceClient(channel);

        const double N1 = 1;
        const double N2 = 1;
        CalculatorReply result = await client.AddAsync(new CalculatorRequest() { N1 = N1, N2 = N2 });
        Console.WriteLine($"{N1} + {N2} = {result.Result}");
    }
}

Listing 5: The grpc-dotnet client

static async Task Main(string[] args)
{
    using (GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001"))
    {
        CalculatorService.CalculatorServiceClient client = new CalculatorService.CalculatorServiceClient(channel);

        const double N1 = 1;
        const double N2 = 1;
        CalculatorReply result = await client.AddAsync(new CalculatorRequest() { N1 = N1, N2 = N2 });
        Console.WriteLine($"{N1} + {N2} = {result.Result}");
    }
}

Once you have an instance of a client, it's simply a matter of calling the methods on it just like you would do with a WCF client proxy. These methods come with both asynchronous and synchronous overloads.

If you now start the server and the client (in that order) and see 1 + 1 = 2 getting printed to the console in the client app, you have successfully migrated the sample service from WCF to gRPC.

Transfer Modes

gRPC supports four types of service calls. The migrated calculator service uses unary RPCs where the client sends a single CalculatorRequest to the server and gets a single CalculatorReply back. This corresponds to the default buffered transfer mode in WCF.

Then there are streaming service methods where the client sends a single request to the server and gets a stream of response messages back. This is similar to the behavior that you get in WCF when you return a Stream from a service operation and use a binding that supports the streamed transfer mode. Client streaming, where the client writes a sequence of request messages and sends them to the server using a provided stream and getting a single response back, is also supported.

The final option is bidirectional streaming where the client and the server operate independently on the same duplex channel using two concurrent streams. Each gRPC call is single HTTP request and the server could either wait until it has received all the request messages from the client before responding or it could write response messages back to the client while still receiving requests. gRPC guarantees that the order of messages in each stream is preserved.

You enable streaming RPCs by simply adding the keyword stream in front of the message type(s) in the service definition in the .proto file. To use server streaming, you add the stream keyword before the response type; to add client streaming, you add it before the request type; and for bidirectional streaming, you add it both before the request type and the response type:

service SampleService {
  rpc ServerStream (Request) returns (stream Reply) {}
  rpc ClientStream (stream Request) returns (Reply) {}
  rpc BirectionalStream (stream Request) returns (stream Reply) {}
}

Let's now take a look at how you could migrate another example from MSDN that uses a duplex service contract to be able to send messages back to the client from the server:

[ServiceContract(Namespace = "http://...", SessionMode = SessionMode.Required, CallbackContract = typeof(ICalculatorDuplexCallback))]
public interface ICalculatorDuplex
{
    [OperationContract(IsOneWay = true)]
    void Clear();
    
    [OperationContract(IsOneWay = true)]
    void AddTo(double n);
    
    [OperationContract(IsOneWay = true)]
    void SubtractFrom(double n);
    
    [OperationContract(IsOneWay = true)]
    void MultiplyBy(double n);
    
    [OperationContract(IsOneWay = true)]
    void DivideBy(double n);
}

The callback interface is defined like this:

public interface ICalculatorDuplexCallback
{
    [OperationContract(IsOneWay = true)]
    void Equals(double result);
    
    [OperationContract(IsOneWay = true)]
    void Equation(string eqn);
}

Lifetime

When migrating this service, you may be tempted to define the five methods of the ICalculatorDuplex contract in a new .proto file. There's one caveat here. The WCF service implementation is decorated with a ServiceBehaviorAttribute with an InstanceContextMode of InstanceContextMode.PerSession. This means that a new instance of the service class is created for each client session. In Grpc.Core, a service always behaves like a singleton; in other words, a single instance of the service class is shared by all clients. This is how the native C-core library that's used internally by the C# code in the Grpc.Core package is implemented. ASP.NET Core and the Grpc.AspNetCore.Server package, which is referenced by default in the project template, creates a new instance of the gRPC service class per client request by default. You can configure the lifetime of a service in the ConfigureServices method of the Startup class just like you would do with any other dependency in any other ASP.NET Core app; regardless of how the service class is instantiated, a client session in gRPC is always equivalent to a single call.

Regardless of how the service class is instantiated, a client session in gRPC is always equivalent to a single call.

There's a UserState property of the ServerCallContext parameter that returns an IDictionary<object, object> that you can use to store any state, but it will only be persisted for the duration of the - potentially long-lived - call to that particular service method.

So how can you implement this service? One option is to define a single service method and use a field in the request message to specify the arithmetic operation to be performed on the server-side. Luckily protocol buffers support enumerations:

enum Operation {
    ADD = 0;
    SUBTRACT = 1;
    MULTIPLY = 2;
    DIVIDE = 3;
    CLEAR = 4;
}
message BidirectionalCalculatorRequest {
  Operation operation = 1;
  double n = 2;
} 
message BidirectionalCalculatorReply {
  double result = 1;
  string eqn = 2;
}

The generated method from the gRPC service in the next snippet now accepts a stream reader and a stream writer - both are asynchronous - and may be implemented something like in Listing 6.

service BidirectionalCalculatorService {
  rpc Calculate (stream BidirectionalCalculatorRequest) returns (stream BidirectionalCalculatorReply) {}
}

Listing 6: The bidirectonal service

public class BidirectionalCalculatorService : BidirectionalCalculatorServiceBase
{
    public override async Task Calculate(IAsyncStreamReader<BidirectionalCalculatorRequest> requestStream, IServerStreamWriter<BidirectionalCalculatorReply>  responseStream, ServerCallContext context)
    {
        double result = 0.0D;
        string equation = result.ToString();

        while (await requestStream.MoveNext().ConfigureAwait(false))
        {
            var request = requestStream.Current;
            double n = request.N;
            switch (request.Operation)
            {
                case Operation.Add:
                    result += n;
                    equation += " + " + n.ToString();
                    break;
                case Operation.Subtract:
                    result -= n;
                    equation += " - " + n.ToString();
                    break;
                case Operation.Multiply:
                    result *= n;
                    equation += " * " + n.ToString();
                    break;
                case Operation.Divide:
                    if (n == 0)
                        throw new RpcException(new Status(StatusCode.InvalidArgument, "Divide by zero attempt."));
                    result /= n;
                    equation += " / " + n.ToString();
                    break;
                case Operation.Clear:
                    equation += " = " + result.ToString();
                    break;
                default:
                    continue;
            }
            await responseStream.WriteAsync(new BidirectionalCalculatorReply()
            {
                Result = result,
                Eqn = equation
            }).ConfigureAwait(false);
            
            //reset state
            if (request.Operation == Operation.Clear)
            {
                result = 0.0D;
                equation = result.ToString();
            }
        }
    }
}

If you target .NET Standard 2.1, you can replace the call to MoveNext and make use of the new async streams support that was introduced in C# 8.0:

await foreach (var message in requestStream.ReadAllAsync()) { ... }

Calling the Calculate() method on the client-side gets you back an AsyncDuplexStreamingCall<BidirectionalCalculatorRequest, BidirectionalCalculatorReply>. This class provides a ResponseStream property that returns an IAsyncStreamReader<BidirectionalCalculatorReply> that you can use to consume response messages from the service. It also has a RequestStream property that returns an IClientStreamWriter<BidirectionalCalculatorRequest> that you can use to send requests the other way.

Callbacks

To be able to read data while you're writing, you need to consume the response stream on another thread than the one on which you write to the request stream. WCF uses the thread pool and synchronization contexts to handle this. In a gRPC client app, you can create your own CallbackHandler class. The one in Listing 7 takes an IAsyncStreamReader<BidirectionalCalculatorReply> and uses it to consume the stream in a Task. You create an instance of it in the client app and await this Task, as shown in Listing 8. Calling CompleteAsync() on the IClientStreamWriter<BidirectionalCalculatorRequest> returned from the RequestStream property terminates the connection. If you run the sample code, you should see the results getting printed to the console.

Listing 7: The callback handler

internal class CallbackHandler
{
    readonly IAsyncStreamReader<BidirectionalCalculatorReply> _responseStream;
    readonly CancellationToken _cancellationToken;

    public CallbackHandler(IAsyncStreamReader<BidirectionalCalculatorReply> responseStream) : this(responseStream, CancellationToken.None) { }

    public CallbackHandler(IAsyncStreamReader<BidirectionalCalculatorReply> responseStream, CancellationToken cancellationToken)
    {
        _responseStream = responseStream ?? throw new ArgumentNullException(nameof(responseStream));
        _cancellationToken = cancellationToken;
        Task = Task.Run(Consume, _cancellationToken);
    }

    public Task Task { get; }

    async Task Consume()
    {
        while (await _responseStream.MoveNext(_cancellationToken).ConfigureAwait(false))
           Console.WriteLine("Equals({0}), Equation({1})", _responseStream.Current.Result, _responseStream.Current.Eqn);
    }
}

Listing 8: The bidirectional client

BidirectionalCalculatorServiceClient bidirectionalClient = new BidirectionalCalculatorServiceClient(channel);

using (var duplexStream = bidirectionalClient.Calculate())
{
    var callbackHandler = new CallbackHandler(duplexStream.ResponseStream);

    await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
    {
        Operation = Operation.Add,
        N = 2
    });

    await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
    {
        Operation = Operation.Multiply,
        N = 2
    });

    await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
    {
        Operation = Operation.Subtract,
        N = 2
    });

    await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
    {
        Operation = Operation.Clear
    });

    await duplexStream.RequestStream.WriteAsync(new BidirectionalCalculatorRequest()
    {
        Operation = Operation.Add,
        N = 10
    });

    await duplexStream.RequestStream.CompleteAsync();    
    await callbackHandler.Task;
}

Security

Passing a ChannelCredentials.Insecure to the constructor of the Grpc.Core.Channel class unsurprisingly creates an unsecure channel with no encryption. There is an SslCredentials class that you can use to apply transport security on the channel using SSL/TLS. Using it requires you to create a certificate and a key file.

The project template for grpc-dotnet uses TLS with a default HTTPS development certificate that's installed with the .NET Core SDK. You are prompted to trust it when you start the ASP.NET Core host process unless you have done this before. How to create and install certificates is out of the scope of this article, but you can refer to the docs at https://bit.ly/2LktGpq for how to configure Kestrel.

Besides channel credentials, you can also apply call credentials to an individual service method call. Each method in the generated client class has an overload that accepts a CallOptions object, which in turn accepts a CallCredentials as one of the arguments in its constructor. CallCredentials is an abstract class and the only built-in-concrete implementation is a private AsyncAuthInterceptorCredentials class that you create using the static CallCredentials.FromInterceptor method. This, in turn, accepts an AsyncAuthInterceptor that can attach metadata, such as for example a bearer token, to outgoing calls:

AsyncAuthInterceptor asyncAuthInterceptor = new AsyncAuthInterceptor((context, metadata) =>
{
    metadata.Add("key", "value");
    return Task.FromResult(default(object));
});
CallCredentials callCredentials = CallCredentials.FromInterceptor(asyncAuthInterceptor);
CallOptions callOptions = new CallOptions(credentials: allCredentials);
 
using (var duplexStream = bidirectionalClient.Calculate(callOptions))
{
    ...
}

Note that the interceptor won't ever get called and the call credentials will be ignored if you don't secure the channel.

You can also compose channel and call credentials using the static ChannelCredentials.Create method. It returns a ChannelCredentials that applies the call credentials for each call made on the channel. For more information and an example of how to use these types and methods, I recommend that you check out the tests in GitHub repository. The API documentation at https://bit.ly/2H6jCit may also be helpful.

If your applications live inside an enterprise with firewalls around it, an option to using certificates and securing the transport channel may be to just send metadata in the requests. If you register your app with Azure Active Directory, you could, for example, use MSAL.NET to acquire a token and implement Windows authentication this way.

You can attach metadata to a service call using the CallOptions class:

CallOptions callOptions = new CallOptions(new Metadata() { 
    new Metadata.Entry("key", "value") 
});

Retrieving it on the server-side is easy using the RequestHeaders property of the ServerCallContext, which returns an IList<Metadata.Entry> (or actually a Metadata object that implements IList<Metadata.Entry>) where Entry has a Key and Value property.

Settings

When it comes to settings, such as, for example, specifying the maximum accepted message length that the channel can send or receive, the Channel class in Grpc.Core accepts an IEnumerable<ChannelOption> in some of its constructor overloads. There is a ChannelOptions (note the plural noun) that defines constants for the names of the most commonly used options:

Channel channel = new Channel("127.0.0.1:50051", ChannelCredentials.Insecure, new ChannelOption[]
{
    new ChannelOption(ChannelOptions.MaxSendMessageLength, 100),
    new ChannelOption(ChannelOptions.MaxReceiveMessageLength, 100)
});

All supported options are defined in the native grpc_types.h header file.

In ASP.NET Core, you configure gRPC settings using the overload of the AddGrpc method that accepts an AddGrpcGrpcServiceOptions:

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.ReceiveMaxMessageSize = 100;
        options.SendMaxMessageSize = 100;
    });
}

Here, you can also add interceptors that intercept the execution of an RPC call to perform things, such as logging, validation and collection of metrics.

Summary

In this article, you've seen how to use protocol buffers and gRPC to build microservices or migrate existing service-oriented applications built using WCF and the .NET Framework to .NET Core and the cloud. Regardless of whether you use the native C-core library or ASP.NET Core 3, gRPC provides an easy setup and an appealing asynchronous API that abstracts away the details of the underlying communication protocol in a nice way. Originally developed by Google and now ported and contributed to by Microsoft, it should definitely be on the list when choosing the technology stack for your next-generation services. At least, it should be if you care about efficiency and streaming response or requests.