Skip to main content

gRPC in .NET

gRPC is a high-performance RPC framework developed by Google that enables service-to-service communication using Protocol Buffers. .NET provides first-class support through gRPC.AspNetCore.

What is gRPC

Overview

gRPC (gRPC Remote Procedure Call) is a high-performance, open-source RPC framework based on the HTTP/2 protocol.

Key Features:

  • High Performance: Fast communication via Protocol Buffers (binary format) and HTTP/2
  • Contract-First: Define services in .proto files and auto-generate code
  • Polyglot: Supports multiple programming languages
  • Streaming: Native support for unary, server, client, and bidirectional streaming
  • Type-Safe: Strong typing with compile-time verification

Protocol Buffers (Protobuf)

Protocol Buffers is Google's language-neutral mechanism for serializing structured data.

Benefits:

  • 3-10x smaller than JSON
  • 20-100x faster to parse
  • Clear schema definition
  • Easy to maintain backward compatibility

gRPC vs REST

FeaturegRPCREST API
ProtocolHTTP/2HTTP/1.1 / HTTP/2
Data FormatProtobuf (Binary)JSON (Text)
PerformanceFastRelatively slow
Browser SupportVia gRPC-WebNative
StreamingUnary & BidirectionalSSE, WebSocket, etc.
Type SafetyStrongWeak (without schema)
Human ReadabilityBinary (unreadable)Text (readable)
CachingComplexSimple (HTTP standard)
Use CaseMicroservice communicationPublic APIs, CRUD operations

Implementing gRPC Services in .NET

1. Project Setup

# Create gRPC service project
dotnet new grpc -n MyGrpcService
cd MyGrpcService

# Required packages (usually included in template)
dotnet add package Grpc.AspNetCore
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

2. Define .proto File

Protos/greet.proto:

syntax = "proto3";

option csharp_namespace = "MyGrpcService";

package greet;

// Greeter service definition
service Greeter {
// Unary RPC
rpc SayHello (HelloRequest) returns (HelloReply);

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

// Client streaming RPC
rpc SendGreetings (stream HelloRequest) returns (HelloReply);

// Bidirectional streaming RPC
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

// Request message
message HelloRequest {
string name = 1;
}

// Response message
message HelloReply {
string message = 1;
}

3. Service Implementation

Services/GreeterService.cs:

using Grpc.Core;
using MyGrpcService;

namespace MyGrpcService.Services;

public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;

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

// Unary RPC
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
_logger.LogInformation("Received request from {Name}", request.Name);

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

// Server streaming RPC
public override async Task SayHellos(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 0; i < 5; i++)
{
// Check for cancellation
if (context.CancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Request cancelled");
break;
}

await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello {request.Name} #{i + 1}"
});

await Task.Delay(TimeSpan.FromSeconds(1));
}
}

// Client streaming RPC
public override async Task<HelloReply> SendGreetings(
IAsyncStreamReader<HelloRequest> requestStream,
ServerCallContext context)
{
var names = new List<string>();

// Read stream from client
await foreach (var request in requestStream.ReadAllAsync())
{
names.Add(request.Name);
}

return new HelloReply
{
Message = $"Received greetings from: {string.Join(", ", names)}"
};
}

// Bidirectional streaming RPC
public override async Task Chat(
IAsyncStreamReader<HelloRequest> requestStream,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync())
{
_logger.LogInformation("Chat message from {Name}", request.Name);

await responseStream.WriteAsync(new HelloReply
{
Message = $"Echo: {request.Name}"
});
}
}
}

4. Program.cs Configuration

Program.cs:

using MyGrpcService.Services;

var builder = WebApplication.CreateBuilder(args);

// Add gRPC services
builder.Services.AddGrpc(options =>
{
// Set global options
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});

// Add interceptors (optional)
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});

var app = builder.Build();

// Map gRPC endpoints
app.MapGrpcService<GreeterService>();

// Enable gRPC reflection (recommended for development only)
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}

// Fallback endpoint for HTTP/1.1
app.MapGet("/", () =>
"Communication with gRPC endpoints must be made through a gRPC client.");

app.Run();

Implementing gRPC Clients in .NET

1. Client Project Setup

# Create console app
dotnet new console -n MyGrpcClient
cd MyGrpcClient

# Required packages
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

2. Add .proto File

Copy or reference the same .proto file from the server to the client project.

.csproj file:

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

3. Client Implementation

Program.cs:

using Grpc.Net.Client;
using MyGrpcService;

// Create channel (reuse recommended)
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);

// 1. Unary RPC
await UnaryCallExample(client);

// 2. Server streaming RPC
await ServerStreamingExample(client);

// 3. Client streaming RPC
await ClientStreamingExample(client);

// 4. Bidirectional streaming RPC
await BidirectionalStreamingExample(client);

static async Task UnaryCallExample(Greeter.GreeterClient client)
{
Console.WriteLine("=== Unary Call ===");

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" });

Console.WriteLine($"Response: {reply.Message}");
}

static async Task ServerStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Server Streaming ===");

using var call = client.SayHellos(
new HelloRequest { Name = "Streaming" });

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

static async Task ClientStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Client Streaming ===");

using var call = client.SendGreetings();

// Send multiple requests
var names = new[] { "Alice", "Bob", "Charlie" };
foreach (var name in names)
{
await call.RequestStream.WriteAsync(
new HelloRequest { Name = name });
Console.WriteLine($"Sent: {name}");
}

await call.RequestStream.CompleteAsync();

var response = await call.ResponseAsync;
Console.WriteLine($"Response: {response.Message}");
}

static async Task BidirectionalStreamingExample(Greeter.GreeterClient client)
{
Console.WriteLine("\n=== Bidirectional Streaming ===");

using var call = client.Chat();

// Send requests and receive responses concurrently
var readTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Response: {response.Message}");
}
});

var names = new[] { "Message1", "Message2", "Message3" };
foreach (var name in names)
{
await call.RequestStream.WriteAsync(
new HelloRequest { Name = name });
Console.WriteLine($"Sent: {name}");
await Task.Delay(1000);
}

await call.RequestStream.CompleteAsync();
await readTask;
}

gRPC Interceptors

Use interceptors to implement cross-cutting concerns (logging, authentication, error handling, etc.).

Server-side Interceptor

Interceptors/LoggingInterceptor.cs:

using Grpc.Core;
using Grpc.Core.Interceptors;

namespace MyGrpcService.Interceptors;

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)
{
_logger.LogInformation(
"Starting call. Method: {Method}, Type: Unary",
context.Method);

try
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await continuation(request, context);
stopwatch.Stop();

_logger.LogInformation(
"Completed call. Method: {Method}, Duration: {Duration}ms",
context.Method,
stopwatch.ElapsedMilliseconds);

return response;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in call. Method: {Method}",
context.Method);
throw;
}
}

public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation(
"Starting call. Method: {Method}, Type: Client Streaming",
context.Method);

return await continuation(requestStream, context);
}

// ServerStreamingServerHandler, DuplexStreamingServerHandler can be implemented similarly
}

Client-side Interceptor

public class ClientLoggingInterceptor : Interceptor
{
private readonly ILogger<ClientLoggingInterceptor> _logger;

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

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_logger.LogInformation(
"Calling {Method}",
context.Method.FullName);

return continuation(request, context);
}
}

// Usage example
var channel = GrpcChannel.ForAddress("https://localhost:7001");
var invoker = channel.Intercept(new ClientLoggingInterceptor(logger));
var client = new Greeter.GreeterClient(invoker);

Error Handling

Server-side

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

try
{
// Business logic
return new HelloReply { Message = $"Hello {request.Name}" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing request");

throw new RpcException(
new Status(StatusCode.Internal, "An error occurred processing your request"),
ex.Message);
}
}

Client-side

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

switch (ex.StatusCode)
{
case StatusCode.InvalidArgument:
Console.WriteLine("Invalid input provided");
break;
case StatusCode.Unavailable:
Console.WriteLine("Service is unavailable");
break;
case StatusCode.DeadlineExceeded:
Console.WriteLine("Request timed out");
break;
default:
Console.WriteLine("Unknown error occurred");
break;
}
}

Using Metadata (Headers)

Server-side: Reading Metadata

public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// Read headers
var headers = context.RequestHeaders;
var authToken = headers.GetValue("authorization");
var userId = headers.GetValue("user-id");

_logger.LogInformation(
"Request from user {UserId} with token {Token}",
userId,
authToken);

// Add response trailers
context.ResponseTrailers.Add("server-time", DateTime.UtcNow.ToString());

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

Client-side: Sending Metadata

var metadata = new Metadata
{
{ "authorization", "Bearer token123" },
{ "user-id", "user456" }
};

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
headers: metadata);

// Get response trailers
using var call = client.SayHelloAsync(
new HelloRequest { Name = "World" },
headers: metadata);

var response = await call.ResponseAsync;
var trailers = call.GetTrailers();
var serverTime = trailers.GetValue("server-time");

Deadlines and Timeouts

Client-side Deadline Configuration

// Method 1: Set deadline for individual calls
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
deadline: DateTime.UtcNow.AddSeconds(5));

// Method 2: Use CallOptions
var options = new CallOptions(
deadline: DateTime.UtcNow.AddSeconds(5),
cancellationToken: cancellationToken);

var reply = await client.SayHelloAsync(
new HelloRequest { Name = "World" },
options);

Server-side Deadline Checking

public override async Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// Long-running operation
for (int i = 0; i < 10; i++)
{
// Check deadline
if (context.CancellationToken.IsCancellationRequested)
{
_logger.LogWarning("Request cancelled or deadline exceeded");
throw new RpcException(
new Status(StatusCode.Cancelled, "Operation was cancelled"));
}

await Task.Delay(1000);
}

return new HelloReply { Message = $"Hello {request.Name}" };
}

Authentication and Authorization

JWT Bearer Authentication

Program.cs:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGrpcService<GreeterService>();

Service Authorization:

using Microsoft.AspNetCore.Authorization;

[Authorize]
public class GreeterService : Greeter.GreeterBase
{
[Authorize(Roles = "Admin")]
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
// Get authenticated user info
var userName = context.GetHttpContext().User.Identity?.Name;

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

Client-side Authentication Token:

var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
if (!string.IsNullOrEmpty(token))
{
metadata.Add("Authorization", $"Bearer {token}");
}
return Task.CompletedTask;
});

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(
new SslCredentials(),
credentials)
});

var client = new Greeter.GreeterClient(channel);

Performance Optimization

1. Reuse Channels

// ❌ Bad: Create channel per call
foreach (var item in items)
{
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);
await client.SayHelloAsync(new HelloRequest { Name = item });
}

// ✅ Good: Reuse channel
using var channel = GrpcChannel.ForAddress("https://localhost:7001");
var client = new Greeter.GreeterClient(channel);

foreach (var item in items)
{
await client.SayHelloAsync(new HelloRequest { Name = item });
}

2. HTTP/2 Connection Pool Configuration

var handler = new SocketsHttpHandler
{
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
EnableMultipleHttp2Connections = true
};

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
HttpHandler = handler
});

3. Message Size Optimization

builder.Services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
options.MaxSendMessageSize = 16 * 1024 * 1024;
options.CompressionProviders = new[]
{
new GzipCompressionProvider(CompressionLevel.Optimal)
};
});

4. Use Streaming

When handling large amounts of data, use streaming instead of sending everything in a single message.

// ❌ Large message (inefficient)
message LargeDataResponse {
repeated DataItem items = 1; // 10,000 items
}

// ✅ Streaming (efficient)
service DataService {
rpc GetData (DataRequest) returns (stream DataItem);
}

Browser Support with gRPC-Web

To call gRPC services directly from browsers, use gRPC-Web.

Server-side Configuration

dotnet add package Grpc.AspNetCore.Web
builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
});
});

var app = builder.Build();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseCors("AllowAll");

app.MapGrpcService<GreeterService>().EnableGrpcWeb();

Client-side (TypeScript/JavaScript)

npm install grpc-web
npm install google-protobuf
npm install @types/google-protobuf
import { GreeterClient } from './generated/greet_grpc_web_pb';
import { HelloRequest } from './generated/greet_pb';

const client = new GreeterClient('https://localhost:7001');

const request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
if (err) {
console.error('Error:', err);
} else {
console.log('Response:', response.getMessage());
}
});

Health Checks

Implement health checks for gRPC services.

dotnet add package Grpc.HealthCheck
dotnet add package Grpc.AspNetCore.HealthChecks
builder.Services.AddGrpc();
builder.Services.AddGrpcHealthChecks()
.AddCheck("Sample", () => HealthCheckResult.Healthy());

var app = builder.Build();

app.MapGrpcService<GreeterService>();
app.MapGrpcHealthChecksService();

Client-side health check:

using Grpc.Health.V1;

var channel = GrpcChannel.ForAddress("https://localhost:7001");
var healthClient = new Health.HealthClient(channel);

var response = await healthClient.CheckAsync(new HealthCheckRequest());
Console.WriteLine($"Health status: {response.Status}");

Best Practices

1. Service Design

  • Balanced Granularity: Keep services at an appropriate level (not too coarse, not too fine)
  • Contract Stability: Make .proto changes carefully and maintain backward compatibility
  • Versioning: Include version in package names (e.g., package myapp.v1;)

2. Performance

  • Reuse Channels: Keep GrpcChannel as singleton or long-lived
  • Use Streaming: Use streaming for large datasets
  • Set Deadlines: Set deadlines for all calls
  • Enable Compression: Enable compression for large messages

3. Error Handling

  • Proper StatusCodes: Return appropriate status codes for different situations
  • Detailed Errors: Enable detailed errors in development environments
  • Retry Logic: Implement appropriate retry logic on the client side

4. Security

  • TLS/SSL: Always use TLS/SSL in production
  • Authentication & Authorization: Protect with JWT, mTLS, API Keys, etc.
  • Rate Limiting: Implement rate limiting to prevent DoS attacks

5. Monitoring and Logging

  • Structured Logging: Record logs with sufficient context
  • Distributed Tracing: Implement distributed tracing with OpenTelemetry, etc.
  • Metrics: Collect metrics like latency, error rates

Troubleshooting

HTTP/2 Disabled

Problem: gRPC requires HTTP/2 but it may be disabled.

Solution:

// Kestrel configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(7001, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
listenOptions.UseHttps();
});
});

Certificate Errors During Development

Problem: SSL certificate errors in development environment.

Solution:

// Development only: Skip certificate validation
var httpHandler = new HttpClientHandler();
if (builder.Environment.IsDevelopment())
{
httpHandler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}

var channel = GrpcChannel.ForAddress("https://localhost:7001", new GrpcChannelOptions
{
HttpHandler = httpHandler
});

Debugging gRPC Calls

Use Grpc.Net.Client.Web to debug gRPC calls:

builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
});

// Enable logging
builder.Logging.AddFilter("Grpc", LogLevel.Debug);

When to Use gRPC for Microservices

gRPC is not a silver bullet. Understanding the appropriate use cases and choosing based on each microservice's characteristics is crucial.

Optimal Scenarios for gRPC

1. Internal Microservice-to-Microservice Communication

Why Recommended:

  • Contract-first design provides significant benefits for intra-organization service communication
  • Clear API contracts via .proto files make service dependencies explicit
  • HTTP/2 multiplexing significantly improves latency and throughput

Example:

┌─────────────┐ gRPC ┌──────────────┐
│ API │────────────────→│ Order │
│ Gateway │ │ Service │
│ (REST) │←────────────────│ (Internal) │
└─────────────┘ └──────────────┘
│ gRPC

┌──────────────┐
│ Inventory │
│ Service │
└──────────────┘

Implementation Points:

  • External APIs use REST (API Gateway), internal communication uses gRPC
  • Combine with service mesh (Linkerd, Istio) for enhanced security and monitoring

2. High-Frequency, Low-Latency Services

Why Recommended:

  • Protocol Buffers binary serialization is 3-10x faster than JSON
  • Ideal for real-time processing: financial transactions, IoT data, game servers

Examples:

  • Real-time Price Streaming Service: Stock prices, exchange rates with high-frequency updates
  • IoT Data Collection Service: Large-scale sensor data collection and processing
  • Real-time Notification Service: Chat, alerts, live feeds
// Real-time price streaming example
service PriceStream {
// Server streaming: continuous price updates
rpc StreamPrices (PriceRequest) returns (stream PriceUpdate);
}

message PriceUpdate {
string symbol = 1;
double price = 2;
google.protobuf.Timestamp timestamp = 3;
}

3. Services Requiring Streaming

Why Recommended:

  • gRPC natively supports unary and bidirectional streaming
  • Simpler implementation than REST's SSE or WebSocket

Examples:

  • Log Aggregation Service: Stream log collection from multiple services
  • File Upload/Download Service: Chunked transfer of large files
  • Chat Service: Real-time messaging
  • Live Data Monitoring: Real-time metrics and health status
// Bidirectional streaming chat example
service ChatService {
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
string user_id = 1;
string room_id = 2;
string content = 3;
google.protobuf.Timestamp timestamp = 4;
}

4. Polyglot (Multi-Language) Environment Services

Why Recommended:

  • Auto-generate client/server code in multiple languages from .proto files
  • Support for major languages: Java, Python, Go, Node.js, C++
  • Avoid type mismatches and serialization issues across languages

Example:

┌──────────────┐
│ .NET │
│ API Service │───┐
└──────────────┘ │
│ Unified API contract
┌──────────────┐ │ via common .proto files
│ Python │───┤
│ ML Service │ │
└──────────────┘ │

┌──────────────┐ │
│ Go │───┘
│ Auth Service│
└──────────────┘

5. Batch or Aggregation Processing Services

Why Recommended:

  • Use client streaming to efficiently send multiple requests
  • Process batch requests together, reducing network overhead

Example:

// Batch processing example
service DataProcessor {
// Client streaming: receive multiple data items for bulk processing
rpc ProcessBatch (stream DataItem) returns (ProcessResult);
}

message DataItem {
string id = 1;
bytes data = 2;
}

message ProcessResult {
int32 total_processed = 1;
int32 successful = 2;
int32 failed = 3;
repeated string error_ids = 4;
}

Scenarios Where gRPC is Not Suitable

1. Public API Services Called Directly from Browsers

Why:

  • gRPC requires HTTP/2; direct browser calls need gRPC-Web
  • gRPC-Web has limitations (no bidirectional streaming)
  • REST has better browser affinity

Recommended Approach:

  • External: REST API, Internal: gRPC
  • Protocol conversion at API Gateway (REST → gRPC)

2. Read-Only Services Where Caching is Critical

Why:

  • REST leverages HTTP caching (CDN, browser cache) easily
  • Caching strategies in gRPC are complex

Examples:

  • Static content delivery services
  • Public API documentation
  • News feeds (low update frequency)

3. Services with Heavy Third-Party Integration

Why:

  • Most third-party APIs are REST-based
  • gRPC support is limited

Recommended Approach:

  • External integration: REST, Internal processing: gRPC

4. Services Requiring Frequent Debugging or Manual Testing

Why:

  • Binary format makes simple testing with tools like Postman difficult
  • Cannot use curl or browsers for direct verification

Mitigation:

  • Enable gRPC Reflection
  • Use dedicated tools like BloomRPC or gRPCurl

Hybrid Approach

For most practical systems, a hybrid approach combining gRPC and REST is optimal.

// Provide both gRPC and REST in the same ASP.NET Core app
var builder = WebApplication.CreateBuilder(args);

// Add gRPC services
builder.Services.AddGrpc();

// Add RESTful API
builder.Services.AddControllers();

var app = builder.Build();

// gRPC endpoints (for internal communication)
app.MapGrpcService<OrderService>();
app.MapGrpcService<InventoryService>();

// REST endpoints (for external public API)
app.MapControllers();

app.Run();

Recommended Patterns:

  • External API: REST (HTTP/1.1, JSON)
  • Internal Microservices: gRPC (HTTP/2, Protobuf)
  • Real-time Streaming: gRPC (Bidirectional streaming)
  • Static Content: REST (Leverage CDN caching)

Decision Flow Chart

Summary: Decision Checklist

Choose gRPC When:

  • ✅ Internal microservice-to-microservice communication
  • ✅ Low latency and high throughput required
  • ✅ Real-time streaming needed
  • ✅ Polyglot (multi-language) environment
  • ✅ Type safety and contract management important
  • ✅ Binary efficiency critical (mobile, IoT)

Choose REST When:

  • ✅ External public API
  • ✅ Direct browser calls
  • ✅ Caching is important
  • ✅ Heavy third-party integration
  • ✅ Human verification and debugging needed
  • ✅ Compatibility with existing REST ecosystem required

Summary

gRPC is very effective in the following scenarios:

Microservice Communication: Fast and type-safe internal communication ✅ Real-time Applications: Real-time communication with bidirectional streaming ✅ Mobile Apps: Efficient communication even on low bandwidth ✅ Polyglot Environments: Systems requiring multi-language support

On the other hand, RESTful APIs are more suitable for:

⚠️ Direct browser calls (when gRPC-Web cannot be used) ⚠️ Caching is a critical requirement ⚠️ Humans need to directly manipulate the API

gRPC implementation in .NET is very mature, and with ASP.NET Core and the Protobuf toolchain, you can build efficient and maintainable services.