Skip to main content

gRPC in .NET: Build High-Performance APIs in C#

Learn how to build gRPC services in .NET with Protocol Buffers. Step-by-step C# tutorial with code, streaming, best practices, and pitfalls. Start now.

If you have ever profiled a busy ASP.NET Core REST API, you know where the time goes: JSON serialization, HTTP/1.1 connection churn, and hand-written client code that drifts out of sync with the server. gRPC .NET fixes all three. It uses Protocol Buffers for compact binary serialization, HTTP/2 for multiplexed connections, and code generation so your client and server share a single contract. In this tutorial you will build a complete gRPC service in C#, add streaming, call it from a .NET client, and learn the best practices and pitfalls that separate a demo from a production system.

What Is gRPC and Why Use It in .NET?

gRPC is a high-performance Remote Procedure Call framework originally built at Google and now maintained by the Cloud Native Computing Foundation. Instead of exposing resources over URLs like REST, you define services with strongly typed methods in a .proto file. The tooling generates the server base class and the client for you.

Microsoft ships first-class support through the grpc-dotnet project, and it is the recommended way to build gRPC services on .NET 8, .NET 9, and .NET 10. Here is why it matters:

  • Speed. Protocol Buffers messages are typically 3 to 10 times smaller than equivalent JSON and parse far faster. Benchmarks from the ASP.NET team regularly show gRPC handling several times the requests per second of JSON APIs at lower CPU.
  • HTTP/2 by default. One TCP connection carries many concurrent calls, with header compression and full-duplex streaming.
  • Contract-first design. The .proto file is the single source of truth. Changing a field regenerates both sides, and the compiler catches mismatches at build time rather than in production.
  • Polyglot. The same contract generates clients for Go, Java, Python, TypeScript, and more, which is why gRPC dominates microservice-to-microservice traffic.

gRPC vs REST: When to Choose Which

gRPC is not a REST replacement for every scenario. Use gRPC for internal service-to-service communication, real-time streaming, mobile backends that care about bandwidth, and any hot path where serialization cost shows up in your profiler. Stick with REST or minimal APIs for public browser-facing endpoints, third-party integrations that expect JSON, and simple CRUD where human readability and curl-ability matter more than throughput. Many teams run both: gRPC inside the cluster, REST at the edge.

Step 1: Create an ASP.NET Core gRPC Service

The .NET SDK includes a ready-made template. Open a terminal and run:

dotnet new grpc -n OrderService
cd OrderService
dotnet run

The template gives you a working Greeter service. We will replace it with something more realistic: an order lookup service. Open the project file and confirm these packages are referenced. The template adds them automatically, but it helps to know what each one does:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <!-- Grpc.AspNetCore bundles Grpc.Tools (the protoc code generator),
         Google.Protobuf, and the ASP.NET Core server integration -->
    <PackageReference Include="Grpc.AspNetCore" Version="2.*" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
  </ItemGroup>
</Project>

The Protobuf item is the important line. GrpcServices="Server" tells the build to generate the abstract service base class. On the client project you set it to Client, and for a shared contracts library you use Both.

Step 2: Define the Contract with Protocol Buffers

Delete Protos/greet.proto and create Protos/orders.proto. This is the heart of any gRPC C# project, so read the comments carefully:

syntax = "proto3";

// Sets the C# namespace of the generated classes.
option csharp_namespace = "OrderService.Grpc";

package orders;

import "google/protobuf/timestamp.proto";

service Orders {
  // Unary: one request, one response.
  rpc GetOrder (GetOrderRequest) returns (OrderReply);

  // Server streaming: one request, a stream of responses.
  rpc WatchOrderStatus (GetOrderRequest) returns (stream OrderStatusUpdate);

  // Client streaming: a stream of requests, one response.
  rpc CreateOrders (stream CreateOrderRequest) returns (CreateOrdersSummary);
}

message GetOrderRequest {
  int32 order_id = 1;
}

message OrderReply {
  int32 order_id = 1;
  string customer_email = 2;
  double total = 3;
  OrderStatus status = 4;
  google.protobuf.Timestamp created_at = 5;
  repeated OrderLine lines = 6;
}

message OrderLine {
  string sku = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

message OrderStatusUpdate {
  int32 order_id = 1;
  OrderStatus status = 2;
  google.protobuf.Timestamp changed_at = 3;
}

message CreateOrderRequest {
  string customer_email = 1;
  repeated OrderLine lines = 2;
}

message CreateOrdersSummary {
  int32 created_count = 1;
  repeated int32 order_ids = 2;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  PENDING = 1;
  PAID = 2;
  SHIPPED = 3;
  DELIVERED = 4;
}

A few things worth understanding about why the file looks this way:

  • Field numbers, not names, go on the wire. The = 1, = 2 tags are what Protocol Buffers serializes. You can rename a field freely, but you must never reuse or renumber a tag once it ships.
  • Enums must start at zero. Proto3 treats zero as the default, so an explicit UNSPECIFIED value protects you from silently misreading a missing field as PENDING.
  • Well-known types. google.protobuf.Timestamp maps cleanly to DateTime and DateTimeOffset in C# through extension methods, so you avoid ambiguity about time zones and formats.
  • snake_case in proto, PascalCase in C#. The generator converts customer_email to CustomerEmail automatically.

Step 3: Implement the gRPC Service in C#

Build the project once so the generated Orders.OrdersBase class exists, then create Services/OrdersService.cs:

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using OrderService.Grpc;

namespace OrderService.Services;

public sealed class OrdersService : Orders.OrdersBase
{
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrdersService> _logger;

    public OrdersService(IOrderRepository repository, ILogger<OrdersService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public override async Task<OrderReply> GetOrder(
        GetOrderRequest request, ServerCallContext context)
    {
        // Always honor the cancellation token; the client may have hung up.
        var order = await _repository.FindAsync(request.OrderId, context.CancellationToken);

        if (order is null)
        {
            // gRPC has its own status codes. Do not throw generic exceptions.
            throw new RpcException(new Status(
                StatusCode.NotFound, $"Order {request.OrderId} was not found."));
        }

        var reply = new OrderReply
        {
            OrderId = order.Id,
            CustomerEmail = order.CustomerEmail,
            Total = (double)order.Total,
            Status = order.Status,
            CreatedAt = Timestamp.FromDateTimeOffset(order.CreatedAt)
        };

        // 'repeated' fields are read-only collections you Add to, not assign.
        reply.Lines.AddRange(order.Lines.Select(l => new OrderLine
        {
            Sku = l.Sku,
            Quantity = l.Quantity,
            UnitPrice = (double)l.UnitPrice
        }));

        return reply;
    }

    public override async Task WatchOrderStatus(
        GetOrderRequest request,
        IServerStreamWriter<OrderStatusUpdate> responseStream,
        ServerCallContext context)
    {
        // Server streaming: push updates until the client disconnects.
        await foreach (var change in _repository
            .StatusChangesAsync(request.OrderId, context.CancellationToken))
        {
            await responseStream.WriteAsync(new OrderStatusUpdate
            {
                OrderId = request.OrderId,
                Status = change.Status,
                ChangedAt = Timestamp.FromDateTimeOffset(change.At)
            });
        }
    }

    public override async Task<CreateOrdersSummary> CreateOrders(
        IAsyncStreamReader<CreateOrderRequest> requestStream,
        ServerCallContext context)
    {
        // Client streaming: read many requests, reply once at the end.
        var summary = new CreateOrdersSummary();

        await foreach (var req in requestStream.ReadAllAsync(context.CancellationToken))
        {
            var id = await _repository.CreateAsync(req, context.CancellationToken);
            summary.OrderIds.Add(id);
            summary.CreatedCount++;
        }

        _logger.LogInformation("Created {Count} orders via stream", summary.CreatedCount);
        return summary;
    }
}

Now register the service in Program.cs. The template already does most of this:

using OrderService.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc(options =>
{
    // Surface exception messages to clients in Development only.
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB, the default
});

builder.Services.AddSingleton<IOrderRepository, InMemoryOrderRepository>();

var app = builder.Build();

app.MapGrpcService<OrdersService>();
app.MapGet("/", () => "This server hosts gRPC endpoints. Use a gRPC client to connect.");

app.Run();

Run dotnet run and Kestrel will listen on HTTPS with HTTP/2 enabled. gRPC requires HTTP/2, which is why the template configures Kestrel for it out of the box.

Step 4: Call the Service from a .NET gRPC Client

Create a console project and add the client-side packages:

dotnet new console -n OrderClient
cd OrderClient
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Copy the same orders.proto into a Protos folder and reference it with GrpcServices="Client" in the project file. Then write the client:

using Grpc.Core;
using Grpc.Net.Client;
using OrderService.Grpc;

// A GrpcChannel is expensive to create and cheap to reuse.
// Create ONE per server address and share it for the app's lifetime.
using var channel = GrpcChannel.ForAddress("https://localhost:7042");
var client = new Orders.OrdersClient(channel);

// --- Unary call with a deadline ---
try
{
    var order = await client.GetOrderAsync(
        new GetOrderRequest { OrderId = 42 },
        deadline: DateTime.UtcNow.AddSeconds(5));

    Console.WriteLine($"Order {order.OrderId} for {order.CustomerEmail}: " +
                      $"{order.Status}, total {order.Total:C}, " +
                      $"placed {order.CreatedAt.ToDateTimeOffset():g}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine($"Not found: {ex.Status.Detail}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
    Console.WriteLine("The server took too long to respond.");
}

// --- Server streaming ---
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var watch = client.WatchOrderStatus(
    new GetOrderRequest { OrderId = 42 },
    cancellationToken: cts.Token);

await foreach (var update in watch.ResponseStream.ReadAllAsync(cts.Token))
{
    Console.WriteLine($"Order {update.OrderId} is now {update.Status} " +
                      $"at {update.ChangedAt.ToDateTimeOffset():T}");
}

// --- Client streaming ---
using var create = client.CreateOrders();

foreach (var email in new[] { "a@example.com", "b@example.com" })
{
    var req = new CreateOrderRequest { CustomerEmail = email };
    req.Lines.Add(new OrderLine { Sku = "SKU-100", Quantity = 2, UnitPrice = 9.99 });
    await create.RequestStream.WriteAsync(req);
}

await create.RequestStream.CompleteAsync();   // Signal "no more requests"
var summary = await create;                    // Await the single response
Console.WriteLine($"Created {summary.CreatedCount} orders: {string.Join(", ", summary.OrderIds)}");

In a real application, prefer the gRPC client factory instead of new-ing channels. Install Grpc.Net.ClientFactory and register the client with dependency injection:

builder.Services
    .AddGrpcClient<Orders.OrdersClient>(o =>
    {
        o.Address = new Uri("https://orders.internal:443");
    })
    .ConfigureChannel(c =>
    {
        // Retry transient failures automatically.
        c.ServiceConfig = new ServiceConfig
        {
            MethodConfigs =
            {
                new MethodConfig
                {
                    Names = { MethodName.Default },
                    RetryPolicy = new RetryPolicy
                    {
                        MaxAttempts = 4,
                        InitialBackoff = TimeSpan.FromMilliseconds(200),
                        MaxBackoff = TimeSpan.FromSeconds(2),
                        BackoffMultiplier = 2,
                        RetryableStatusCodes = { StatusCode.Unavailable }
                    }
                }
            }
        };
    });

The factory manages channel lifetime, integrates with HttpClientFactory handlers and Polly, and lets you inject Orders.OrdersClient anywhere.

gRPC .NET Best Practices for Production

  • Reuse channels. Every GrpcChannel opens its own HTTP/2 connection. Creating one per call is the single most common performance mistake in gRPC C# code.
  • Always set deadlines. Without a deadline, a hung downstream service ties up your threads forever. Deadlines propagate across services automatically when you forward the ServerCallContext token.
  • Use gRPC status codes. Throw RpcException with NotFound, InvalidArgument, PermissionDenied, and so on. Unhandled exceptions become an opaque Unknown status that clients cannot act on.
  • Add interceptors for cross-cutting concerns. Logging, authentication, and exception mapping belong in a server Interceptor, not copied into each method.
  • Secure it. Use TLS everywhere and plug in ASP.NET Core authentication with [Authorize] on the service class. JWT bearer tokens work exactly as they do for controllers.
  • Enable health checks and reflection. Grpc.AspNetCore.HealthChecks lets Kubernetes probe your service, and Grpc.AspNetCore.Server.Reflection lets tools like grpcurl and Postman discover your contract.
  • Consider gRPC-Web or JSON transcoding for browsers. Browsers cannot speak native gRPC. Microsoft.AspNetCore.Grpc.JsonTranscoding exposes your gRPC methods as RESTful JSON endpoints from the same code, with OpenAPI support.

Common Pitfalls and How to Avoid Them

  • Calling an HTTP/1.1 endpoint. If you see Status(StatusCode="Internal", Detail="Bad gRPC response. HTTP status code: 404") or protocol errors, the server is not serving HTTP/2. Check the Kestrel Protocols setting and any reverse proxy in front of it.
  • Using double for money. Protocol Buffers has no decimal type. For financial data, send an integer number of cents or define a custom DecimalValue message with units and nanos, as Microsoft documents.
  • Forgetting the zero-value rule. Proto3 does not serialize default values. A missing int32 arrives as 0 and a missing string as empty. Use optional fields or wrapper types when you must distinguish "not set" from zero.
  • Large messages. gRPC caps messages at 4 MB by default. Do not raise it to 500 MB. Stream large payloads in chunks instead, which also keeps memory flat.
  • Breaking the contract. Removing a field or changing its type breaks older clients. Mark old fields reserved, add new ones with fresh tags, and never change a tag's type.
  • Blocking on async calls. Calling .Result on a gRPC call can deadlock and hides cancellation. Use await end to end.

Conclusion: Key Takeaways on gRPC .NET

gRPC .NET gives C# developers a fast, strongly typed, contract-first way to build APIs, and the tooling in ASP.NET Core makes it nearly as easy to set up as a minimal API. Here is what to remember:

  • Define your contract in a .proto file and let the compiler generate both server and client code.
  • Protocol Buffers and HTTP/2 deliver smaller payloads, fewer connections, and higher throughput than JSON over HTTP/1.1.
  • Choose the right call type: unary for request-reply, server streaming for live updates, client streaming for bulk uploads, and bidirectional streaming for chat-style workloads.
  • Reuse channels, set deadlines, and return proper gRPC status codes to keep your services resilient.
  • Respect Protocol Buffers evolution rules so you can ship new versions without breaking existing clients.

Start by converting one internal service-to-service call in your system to gRPC .NET and measure the difference. Most teams see the latency and CPU savings immediately, and the generated client code pays for itself on the very first contract change.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...