gRPC services in C# with ASP.NET Core aren’t just faster than REST; they fundamentally change how you think about remote procedure calls by treating them like local method invocations.
Let’s see this in action. Imagine we have a simple Greeter service defined in a .proto file:
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
When you build an ASP.NET Core gRPC project, the dotnet-grpc tool (or equivalent build integration) generates C# code from this .proto file. This includes an abstract GreeterBase class and the HelloRequest/HelloReply message types.
Your server implementation will inherit from GreeterBase and override the SayHello method:
using Grpc.Core;
using Greet; // Assuming your proto namespace is 'greet'
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}
}
And in your Program.cs (or Startup.cs for older ASP.NET Core versions), you’ll map this service:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(); // Registers gRPC services
var app = builder.Build();
app.MapGrpcService<GreeterService>(); // Maps your service to a gRPC endpoint
app.Run();
On the client side, you’ll add a gRPC reference to your project. The generated client code will look something like this:
using Grpc.Net.Client;
using Greet;
var channel = GrpcChannel.ForAddress("https://localhost:7001"); // Replace with your server address
var client = new Greeter.GreeterClient(channel);
var request = new HelloRequest { Name = "Alice" };
var response = await client.SayHelloAsync(request);
Console.WriteLine($"Greeting: {response.Message}");
This client code makes a call to the SayHelloAsync method, which internally handles serialization (Protobuf), HTTP/2 framing, and network transport. The result is deserialized back into a HelloReply object. You’re writing code as if you’re calling a local method, but the framework abstracts away all the network complexity.
The core problem gRPC solves is efficient, strongly-typed inter-service communication. Unlike REST, where you’re dealing with loosely-typed JSON payloads and HTTP status codes, gRPC uses Protocol Buffers for serialization, which is significantly more compact and faster. The use of HTTP/2 enables features like multiplexing (multiple requests/responses over a single connection), header compression, and server/client streaming, all of which are critical for high-performance microservices.
The contract-first approach, defining services and messages in .proto files, ensures that both client and server agree on the communication schema before any code is written. This eliminates a huge class of integration bugs. The generated code provides type safety, so you can’t accidentally send a string where an integer is expected, or misspell a field name.
The magic behind GrpcChannel.ForAddress and client.SayHelloAsync is the gRPC client factory and the underlying HttpClientHandler configured for HTTP/2. When you call SayHelloAsync, the gRPC library serializes the HelloRequest using Protobuf, constructs an HTTP/2 POST request with specific headers (like :path and content-type: application/grpc), and sends it over the established GrpcChannel. The server receives the request, deserializes the Protobuf payload, invokes your SayHello implementation, serializes the HelloReply, and sends it back. The client then deserializes the response.
One of the most powerful, yet often overlooked, aspects of gRPC is its streaming capabilities. You can define RPCs that support client-streaming (client sends multiple messages, server responds once), server-streaming (client sends one message, server sends multiple responses), or bi-directional streaming (both client and server send multiple messages). This is implemented using IAsyncStreamReader<T> and IAsyncStreamWriter<T> on the server, and AsyncServerStreamingCall or AsyncClientStreamingCall on the client. For example, a server-streaming RPC might look like:
public override async Task GetStreamOfItems(StreamRequest request, IServerStreamWriter<StreamReply> responseStream, ServerCallContext context)
{
for (int i = 0; i < 10; i++)
{
await responseStream.WriteAsync(new StreamReply { Item = $"Item {i} for {request.Id}" });
await Task.Delay(100); // Simulate work
}
}
The client would then consume this stream:
var streamResponse = client.GetStreamOfItems(new StreamRequest { Id = "abc" });
await foreach (var item in streamResponse.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Received: {item.Item}");
}
This allows for efficient handling of large datasets or real-time updates without the overhead of repeated HTTP requests.
The next step after mastering basic gRPC services is understanding how to secure them, often using TLS, and how to implement more advanced patterns like inter-service authentication and authorization.