Skip to main content

.NET gRPC Services

Introduction

gRPC (gRPC Remote Procedure Calls) is a modern, open-source, high-performance remote procedure call (RPC) framework that can run in any environment. It enables client and server applications to communicate transparently and build connected systems.

In the .NET ecosystem, gRPC offers an efficient way to build services with:

  • High performance: gRPC uses HTTP/2 for transport, Protocol Buffers (protobuf) as the interface description language, and provides features like bidirectional streaming.
  • Strong typing: The service contracts are defined in .proto files, which generate client and server code.
  • Cross-platform compatibility: Services can communicate across different platforms and languages.
  • Streaming capabilities: Support for client, server, and bidirectional streaming.

This guide will walk you through creating, implementing, and consuming gRPC services in .NET applications.

Getting Started with gRPC in .NET

Prerequisites

To follow along, you'll need:

  • .NET 6.0 SDK or later
  • A code editor (like Visual Studio, Visual Studio Code)

Creating a gRPC Service

Let's start by creating a basic gRPC service using the .NET CLI:

bash
# Create a new gRPC service project
dotnet new grpc -o GrpcGreeterService

This command creates a new gRPC service project with a sample "Greeter" service.

Project Structure

After creating the project, you'll have the following key files:

  • Program.cs: The entry point for the application
  • Services/GreeterService.cs: Implementation of the gRPC service
  • Protos/greet.proto: The Protocol Buffer definition for the service
  • GrpcGreeterService.csproj: Project file with gRPC-related packages and settings

Understanding Protocol Buffers (Protobuf)

Protocol Buffers are at the heart of gRPC. Let's examine the default greet.proto file:

protobuf
syntax = "proto3";

option csharp_namespace = "GrpcGreeterService";

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 greeting
message HelloReply {
string message = 1;
}

This proto file defines:

  1. Service: Greeter with a method called SayHello
  2. Messages: HelloRequest (input) and HelloReply (output) with their fields

When you build the project, the .NET gRPC tooling automatically generates C# classes for your service, client, and messages.

Implementing the gRPC Service

The generated GreeterService.cs file contains the implementation of our service:

csharp
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)
{
_logger.LogInformation($"Saying hello to {request.Name}");
return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}
}

Let's modify this service to add another method:

csharp
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}

public override Task<HelloReply> SayHelloAgain(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello again {request.Name}"
});
}

To support the new method, we need to update the proto file:

protobuf
// The greeting service definition
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (HelloReply);
}

Creating a gRPC Client

Let's create a client application to consume our gRPC service:

bash
# Create a new console application
dotnet new console -o GrpcGreeterClient

Add the required NuGet packages:

bash
cd GrpcGreeterClient
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Create a Protos folder in the client project and copy the greet.proto file from the service project. Then update your .csproj file:

xml
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

Now, implement the client:

csharp
using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using GrpcGreeterService;

namespace GrpcGreeterClient
{
class Program
{
static async Task Main(string[] args)
{
// Create a channel
using var channel = GrpcChannel.ForAddress("https://localhost:7042");

// Create a client
var client = new Greeter.GreeterClient(channel);

// Call the SayHello method
var reply = await client.SayHelloAsync(new HelloRequest { Name = "Developer" });
Console.WriteLine($"Greeting: {reply.Message}");

// Call the SayHelloAgain method
var secondReply = await client.SayHelloAgainAsync(new HelloRequest { Name = "Developer" });
Console.WriteLine($"Second greeting: {secondReply.Message}");

Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}

Running the Application

  1. Start the gRPC service:
bash
cd GrpcGreeterService
dotnet run
  1. In another terminal, run the client:
bash
cd GrpcGreeterClient
dotnet run

Expected output:

Greeting: Hello Developer
Second greeting: Hello again Developer
Press any key to exit...

Advanced gRPC Features

Streaming in gRPC

gRPC supports three types of streaming:

  1. Server streaming: The server sends multiple responses to a client's single request
  2. Client streaming: The client sends multiple messages to the server
  3. Bidirectional streaming: Both client and server send multiple messages to each other

Let's add a server streaming RPC to our Greeter service:

protobuf
// In greet.proto
service Greeter {
// Existing methods...

// Server streaming example
rpc SayHellos (HelloRequest) returns (stream HelloReply);
}

Implement the method in the service:

csharp
public override async Task SayHellos(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
var greetings = new[] { "Hello", "Hola", "Bonjour", "Ciao", "Konnichiwa" };

foreach (var greeting in greetings)
{
await responseStream.WriteAsync(new HelloReply
{
Message = $"{greeting} {request.Name}"
});

// Simulate delay between responses
await Task.Delay(1000);
}
}

Now, update the client to handle streaming:

csharp
// Server streaming example
Console.WriteLine("Starting streaming call...");
using var call = client.SayHellos(new HelloRequest { Name = "Developer" });

await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Greeting: {response.Message}");
}

Error Handling in gRPC

gRPC has a standardized error model. Here's an example of how to return errors:

csharp
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Name))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "Name is required"));
}

return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}

Client-side error handling:

csharp
try
{
var reply = await client.SayHelloAsync(new HelloRequest { Name = "" });
Console.WriteLine($"Greeting: {reply.Message}");
}
catch (RpcException ex)
{
Console.WriteLine($"Error: {ex.Status.Detail}");
}

Real-World Example: Product Catalog Service

Let's create a more practical example of a product catalog service:

First, define the proto file (product.proto):

protobuf
syntax = "proto3";

option csharp_namespace = "ProductCatalogService";

package product;

service ProductCatalog {
// Get a single product by ID
rpc GetProduct (GetProductRequest) returns (ProductModel);

// Search for products
rpc SearchProducts (SearchProductsRequest) returns (stream ProductModel);

// Add a new product
rpc AddProduct (ProductModel) returns (AddProductResponse);
}

message GetProductRequest {
int32 product_id = 1;
}

message SearchProductsRequest {
string query = 1;
int32 max_results = 2;
}

message ProductModel {
int32 product_id = 1;
string name = 2;
string description = 3;
double price = 4;
ProductCategory category = 5;
bool in_stock = 6;
}

enum ProductCategory {
UNKNOWN = 0;
ELECTRONICS = 1;
CLOTHING = 2;
FOOD = 3;
BOOKS = 4;
}

message AddProductResponse {
int32 product_id = 1;
bool success = 2;
string error_message = 3;
}

Now implement the service:

csharp
public class ProductCatalogService : ProductCatalog.ProductCatalogBase
{
private readonly ILogger<ProductCatalogService> _logger;
private readonly List<ProductModel> _products = new List<ProductModel>
{
new ProductModel
{
ProductId = 1,
Name = "Surface Laptop 4",
Description = "Premium laptop with touch screen",
Price = 1299.99,
Category = ProductCategory.Electronics,
InStock = true
},
new ProductModel
{
ProductId = 2,
Name = "Professional C# and .NET",
Description = "Programming guide for C# and .NET",
Price = 49.99,
Category = ProductCategory.Books,
InStock = true
}
// More products...
};

public ProductCatalogService(ILogger<ProductCatalogService> logger)
{
_logger = logger;
}

public override Task<ProductModel> GetProduct(GetProductRequest request, ServerCallContext context)
{
_logger.LogInformation($"Looking for product with ID: {request.ProductId}");

var product = _products.FirstOrDefault(p => p.ProductId == request.ProductId);

if (product == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Product with ID {request.ProductId} not found"));
}

return Task.FromResult(product);
}

public override async Task SearchProducts(SearchProductsRequest request, IServerStreamWriter<ProductModel> responseStream, ServerCallContext context)
{
_logger.LogInformation($"Searching for products with query: {request.Query}");

var matchingProducts = _products
.Where(p => p.Name.Contains(request.Query, StringComparison.OrdinalIgnoreCase) ||
p.Description.Contains(request.Query, StringComparison.OrdinalIgnoreCase))
.Take(request.MaxResults > 0 ? request.MaxResults : int.MaxValue);

foreach (var product in matchingProducts)
{
await responseStream.WriteAsync(product);
await Task.Delay(100); // Simulate delay
}
}

public override Task<AddProductResponse> AddProduct(ProductModel request, ServerCallContext context)
{
_logger.LogInformation($"Adding new product: {request.Name}");

// Validate input
if (string.IsNullOrWhiteSpace(request.Name))
{
return Task.FromResult(new AddProductResponse
{
Success = false,
ErrorMessage = "Product name is required"
});
}

// Generate ID (in a real app, this would be from a database)
int newId = _products.Count > 0 ? _products.Max(p => p.ProductId) + 1 : 1;
request.ProductId = newId;

_products.Add(request);

return Task.FromResult(new AddProductResponse
{
ProductId = newId,
Success = true
});
}
}

Client code to interact with this service:

csharp
static async Task Main(string[] args)
{
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new ProductCatalog.ProductCatalogClient(channel);

// Get product by ID
try
{
var product = await client.GetProductAsync(new GetProductRequest { ProductId = 1 });
Console.WriteLine($"Found product: {product.Name}, ${product.Price}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
Console.WriteLine("Product not found");
}

// Search for products
Console.WriteLine("\nSearching for 'laptop'...");
using var searchCall = client.SearchProducts(new SearchProductsRequest
{
Query = "laptop",
MaxResults = 5
});

await foreach (var product in searchCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"- {product.Name}: {product.Description}");
}

// Add a new product
var addResponse = await client.AddProductAsync(new ProductModel
{
Name = "Mechanical Keyboard",
Description = "RGB gaming keyboard with mechanical switches",
Price = 129.99,
Category = ProductCategory.Electronics,
InStock = true
});

if (addResponse.Success)
{
Console.WriteLine($"\nProduct added successfully with ID: {addResponse.ProductId}");
}
else
{
Console.WriteLine($"\nFailed to add product: {addResponse.ErrorMessage}");
}
}

Best Practices for gRPC Services

  1. Define clear service boundaries: Each gRPC service should handle a specific domain or function
  2. Version your APIs: Use package names or different service definitions to manage API versions
  3. Use deadlines/timeouts: Always set timeouts for gRPC calls to avoid hanging operations
  4. Implement proper error handling: Return appropriate status codes and error details
  5. Consider message size limits: Be aware of default message size limits for large payloads
  6. Optimize for performance: Use streaming for large datasets
  7. Secure your services: Implement authentication and authorization
  8. Monitor and log: Add telemetry to track performance and troubleshoot issues

Limitations and Considerations

  • Browser support: gRPC requires HTTP/2 features not fully available in browser JavaScript
  • Human readability: Protobuf messages aren't human-readable like JSON
  • Learning curve: The protobuf syntax and streaming concepts can be challenging for beginners
  • Mature alternatives: REST and GraphQL are more established in some contexts

Summary

In this article, we explored gRPC services in .NET, covering:

  • How to create gRPC services and clients
  • Working with Protocol Buffers
  • Implementing unary and streaming RPCs
  • Error handling in gRPC
  • Building a practical product catalog service
  • Best practices for gRPC service development

gRPC provides excellent performance and strong typing, making it particularly suitable for microservices communication, mobile applications, and scenarios requiring efficient, real-time communication.

Additional Resources

Exercises

  1. Create a bidirectional streaming gRPC service for a chat application
  2. Implement a gRPC service with authentication using JWT tokens
  3. Build a file upload service using client streaming
  4. Create a notification service that sends real-time updates using server streaming
  5. Develop a health checking service for your gRPC application

By mastering gRPC in .NET, you'll be well-equipped to build high-performance, cross-platform services for modern distributed applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)