gRPC in .NET · Lesson 1 of 1
gRPC vs REST — When to Use Each
gRPC vs REST — When to Use Each
When two services need to talk to each other, REST is usually the default. But as systems grow and microservices multiply, REST starts showing cracks: verbose JSON payloads, no type contracts, HTTP/1.1 overhead, no built-in streaming.
gRPC solves all of these. It's a high-performance, type-safe RPC framework that runs over HTTP/2 and uses Protocol Buffers for serialization instead of JSON.
This lesson covers everything you need to make the REST-vs-gRPC decision and start building gRPC services in .NET.
What Is gRPC?
gRPC (Google Remote Procedure Call) is an open-source RPC framework that lets you call methods on a remote server as if they were local function calls.
Key characteristics:
- HTTP/2 — multiplexed streams, binary framing, header compression
- Protocol Buffers (protobuf) — binary serialization, ~10× smaller than JSON
- Strongly typed contracts —
.protofiles define the API - Code generation — client and server stubs generated from
.proto - Streaming — client, server, and bidirectional streaming built in
// orders.proto
syntax = "proto3";
service OrderService {
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc StreamOrders (StreamOrdersRequest) returns (stream OrderResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message OrderResponse {
string order_id = 1;
string customer_id = 2;
double total = 3;
string status = 4;
}gRPC vs REST — the Decision Matrix
| Factor | REST | gRPC |
|--------|------|------|
| Payload size | JSON (verbose) | Binary protobuf (~10× smaller) |
| Speed | HTTP/1.1 | HTTP/2 multiplexed |
| Type safety | None (JSON) | Strong — generated from .proto |
| Streaming | Workarounds (SSE, WS) | Native (4 patterns) |
| Browser support | Full | Requires gRPC-Web proxy |
| Human readable | Yes | No (binary) |
| Code generation | Optional (OpenAPI) | Mandatory (protobuf) |
| Best for | Public APIs, browser clients | Internal microservices, high-throughput |
Choose gRPC when:
- Services are internal (backend-to-backend)
- You need low latency and small payloads
- You need streaming (real-time data, large file transfers)
- You want a strict contract between services
Choose REST when:
- Building a public API consumed by browsers or third parties
- You need human-readable requests for debugging
- Your team is unfamiliar with protobuf tooling
The Four gRPC Communication Patterns
gRPC supports four call types — this is its biggest advantage over REST:
1. Unary (Request/Response)
Standard call — one request, one response. Same as REST.
rpc GetOrder (GetOrderRequest) returns (OrderResponse);2. Server Streaming
One request, stream of responses. Perfect for real-time feeds.
rpc StreamOrderUpdates (OrderId) returns (stream OrderResponse);3. Client Streaming
Stream of requests, one response. Good for uploading batches of data.
rpc UploadOrders (stream CreateOrderRequest) returns (UploadResult);4. Bidirectional Streaming
Both sides stream simultaneously. Chat applications, real-time collaboration.
rpc SyncInventory (stream InventoryUpdate) returns (stream InventoryConfirmation);Building a gRPC Service in ASP.NET Core
1. Create the project
dotnet new grpc -n OrderService
cd OrderServiceThis creates a .proto file in Protos/ and a service implementation.
2. Define your protobuf contract
// Protos/orders.proto
syntax = "proto3";
option csharp_namespace = "OrderService";
package orders;
service Orders {
rpc GetOrder (GetOrderRequest) returns (OrderReply);
rpc CreateOrder (CreateOrderRequest) returns (OrderReply);
rpc StreamOrders (StreamOrdersRequest) returns (stream OrderReply);
}
message GetOrderRequest {
string id = 1;
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
double price = 3;
}
message OrderReply {
string id = 1;
string customer_id = 2;
double total = 3;
string status = 4;
string created_at = 5;
}
message StreamOrdersRequest {
string customer_id = 1;
}Register the proto file in your .csproj:
<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
</ItemGroup>Build the project — gRPC tooling auto-generates C# classes.
3. Implement the service
using Grpc.Core;
using OrderService;
public class OrdersService : Orders.OrdersBase
{
private readonly IOrderRepository _orders;
private readonly ILogger<OrdersService> _logger;
public OrdersService(IOrderRepository orders, ILogger<OrdersService> logger)
{
_orders = orders;
_logger = logger;
}
public override async Task<OrderReply> GetOrder(
GetOrderRequest request, ServerCallContext context)
{
var order = await _orders.GetByIdAsync(request.Id);
if (order is null)
throw new RpcException(new Status(StatusCode.NotFound, $"Order {request.Id} not found"));
return MapToReply(order);
}
public override async Task<OrderReply> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = (decimal)i.Price
}).ToList()
};
await _orders.CreateAsync(order);
_logger.LogInformation("Order {OrderId} created for customer {CustomerId}",
order.Id, order.CustomerId);
return MapToReply(order);
}
// Server streaming — push updates as orders change status
public override async Task StreamOrders(
StreamOrdersRequest request,
IServerStreamWriter<OrderReply> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var orders = await _orders.GetRecentByCustomerAsync(request.CustomerId);
foreach (var order in orders)
await responseStream.WriteAsync(MapToReply(order));
await Task.Delay(TimeSpan.FromSeconds(2), context.CancellationToken);
}
}
private static OrderReply MapToReply(Order order) => new()
{
Id = order.Id.ToString(),
CustomerId = order.CustomerId,
Total = (double)order.Total,
Status = order.Status.ToString(),
CreatedAt = order.CreatedAt.ToString("O")
};
}4. Register in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
options.MaxSendMessageSize = 4 * 1024 * 1024;
});
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
var app = builder.Build();
app.MapGrpcService<OrdersService>();
// Optional: gRPC reflection for tools like grpcurl
app.MapGrpcReflectionService();
app.Run();Calling gRPC from Another .NET Service (Client)
// Client project — add the proto file with GrpcServices="Client"<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Client" />
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
<PackageReference Include="Google.Protobuf" Version="3.28.0" />
<PackageReference Include="Grpc.Tools" Version="2.65.0" PrivateAssets="All" />
</ItemGroup>// Register typed gRPC client
builder.Services.AddGrpcClient<Orders.OrdersClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
// For dev with self-signed certs
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
});
// Use it in a controller
public class CheckoutController : ControllerBase
{
private readonly Orders.OrdersClient _ordersClient;
public CheckoutController(Orders.OrdersClient ordersClient)
=> _ordersClient = ordersClient;
[HttpPost("checkout")]
public async Task<IActionResult> Checkout(CheckoutRequest request)
{
var reply = await _ordersClient.CreateOrderAsync(new CreateOrderRequest
{
CustomerId = request.CustomerId,
Items = { request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
Price = (double)i.Price
})}
});
return Ok(new { OrderId = reply.Id, Total = reply.Total });
}
}Error Handling — gRPC Status Codes
gRPC uses its own status codes instead of HTTP status codes:
// Not found
throw new RpcException(new Status(StatusCode.NotFound, "Order not found"));
// Validation error
throw new RpcException(new Status(StatusCode.InvalidArgument, "CustomerId is required"));
// Unauthorized
throw new RpcException(new Status(StatusCode.Unauthenticated, "Token invalid or expired"));
// Rate limited
throw new RpcException(new Status(StatusCode.ResourceExhausted, "Rate limit exceeded"));
// Catch on the client
try
{
var reply = await client.GetOrderAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
return NotFound(ex.Status.Detail);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
return Unauthorized();
}Interceptors — Middleware for gRPC
Interceptors work like ASP.NET Core middleware but for gRPC calls:
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
=> _logger = logger;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var method = context.Method;
_logger.LogInformation("gRPC call: {Method}", method);
try
{
var response = await continuation(request, context);
_logger.LogInformation("gRPC success: {Method}", method);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "gRPC error: {Method}", method);
throw;
}
}
}
// Register:
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});Authentication in gRPC
// Add JWT bearer to gRPC
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* configure */ });
builder.Services.AddAuthorization();
// In service, use [Authorize] attribute
[Authorize]
public override async Task<OrderReply> GetOrder(...)
// Client — attach JWT token
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
var token = await tokenService.GetTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
});
var channel = GrpcChannel.ForAddress("https://order-service:5001", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(
new SslCredentials(), credentials)
});Testing gRPC Services
# Install grpcurl for manual testing
winget install fullstorydev.grpcurl
# List services
grpcurl -plaintext localhost:5001 list
# Call a method
grpcurl -plaintext -d '{"id": "order-123"}' localhost:5001 orders.Orders/GetOrderFor integration tests:
public class OrdersServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly Orders.OrdersClient _client;
public OrdersServiceTests(WebApplicationFactory<Program> factory)
{
var channel = GrpcChannel.ForAddress("http://localhost",
new GrpcChannelOptions { HttpHandler = factory.Server.CreateHandler() });
_client = new Orders.OrdersClient(channel);
}
[Fact]
public async Task GetOrder_ExistingId_ReturnsOrder()
{
var reply = await _client.GetOrderAsync(new GetOrderRequest { Id = "order-1" });
Assert.Equal("order-1", reply.Id);
}
}Key Takeaways
- Use gRPC for internal microservice communication — the performance and type safety gains are significant at scale
- The
.protofile is your contract — version it in source control, never break backwards compatibility - Server streaming replaces polling — push data as it changes instead of clients polling every N seconds
- gRPC errors are typed — use status codes (
NotFound,InvalidArgument) instead of exceptions - Interceptors are your middleware — logging, auth, validation all go here
The next lesson covers protobuf design patterns — how to version your contracts, handle backwards compatibility, and design messages that don't break clients when you evolve them.