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.