gRPC
16 minute read
Introduction to gRPC
gRPC is a high-performance, open-source Remote Procedure Call (RPC) framework initially developed by Google. It uses Protocol Buffers (protobuf) as its Interface Definition Language (IDL) and underlying message interchange format. gRPC supports multiple programming languages and platforms, making it an excellent choice for building distributed systems and microservices.
Key Features of gRPC
- Contract-first API development using Protocol Buffers
- Efficient binary serialization with smaller payloads than JSON
- Built on HTTP/2 for multiplexing, bidirectional streaming, and header compression
- Strong typing with code generation across multiple languages
- Support for multiple communication patterns: unary, server streaming, client streaming, and bidirectional streaming
- Built-in authentication, load balancing, and health checking
gRPC vs REST
Feature | gRPC | REST |
---|---|---|
Protocol | HTTP/2 | HTTP (1.1, 2, 3) |
Payload format | Protocol Buffers (binary) | Typically JSON (text) |
Contract | Required (.proto files) | Optional (OpenAPI) |
Code generation | Yes, built-in | Optional with third-party tools |
Streaming | Bidirectional native streaming | Limited (with WebSockets or SSE) |
Browser support | Limited (requires gRPC-Web) | Native |
Payload size | Smaller | Larger |
Human readability | No | Yes |
Learning curve | Steeper | Gentler |
When to Use gRPC Instead of REST
gRPC is particularly well-suited for:
- Microservice architectures - For service-to-service communication where performance is critical
- Low-latency, high-throughput communication - When you need efficient binary serialization
- Polyglot environments - When you need to generate consistent client/server stubs across multiple languages
- Bidirectional streaming - When you need real-time communication with streaming support
- Constrained environments - Mobile applications or IoT devices where bandwidth is limited
- Point-to-point real-time communication - When immediate updates are required
Getting Started with gRPC in .NET
ASP.NET Core and .NET provide excellent support for building gRPC services. Let’s look at how to implement gRPC in a .NET application.
Setting Up a gRPC Project
Create a new gRPC service using the .NET CLI:
dotnet new grpc -o GrpcService
cd GrpcService
The template creates a working gRPC service project that you can start immediately.
Defining Services with Protocol Buffers
Create or modify a .proto file to define your service contract:
syntax = "proto3";
option csharp_namespace = "GrpcService";
package weather;
// The weather service definition
service WeatherService {
// Unary call - get current weather
rpc GetCurrentWeather (WeatherRequest) returns (WeatherResponse);
// Server streaming - get weather forecast
rpc GetWeatherStream (WeatherRequest) returns (stream WeatherResponse);
// Client streaming - send weather updates
rpc SendWeatherUpdates (stream WeatherData) returns (UpdateSummary);
// Bidirectional streaming - interactive weather updates
rpc WeatherChat (stream WeatherRequest) returns (stream WeatherResponse);
}
// The request message
message WeatherRequest {
string location = 1;
bool include_forecast = 2;
}
// The response message
message WeatherResponse {
string location = 1;
double temperature_celsius = 2;
string condition = 3;
int32 humidity = 4;
double wind_speed = 5;
}
// Weather data for updates
message WeatherData {
string location = 1;
double temperature_celsius = 2;
string condition = 3;
int64 timestamp = 4;
}
// Summary of updates received
message UpdateSummary {
int32 updates_received = 1;
bool success = 2;
string message = 3;
}
Implementing the gRPC Service
Create a service implementation class that inherits from the auto-generated base class:
using Grpc.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GrpcService
{
public class WeatherService : Weather.WeatherService.WeatherServiceBase
{
private readonly ILogger<WeatherService> _logger;
public WeatherService(ILogger<WeatherService> logger)
{
_logger = logger;
}
// Implement unary call
public override Task<WeatherResponse> GetCurrentWeather(WeatherRequest request, ServerCallContext context)
{
_logger.LogInformation("Getting weather for {Location}", request.Location);
// In a real implementation, you'd fetch data from a weather service
WeatherResponse response = new WeatherResponse
{
Location = request.Location,
TemperatureCelsius = 23.5,
Condition = "Partly Cloudy",
Humidity = 65,
WindSpeed = 12.3
};
return Task.FromResult(response);
}
// Implement server streaming
public override async Task GetWeatherStream(
WeatherRequest request,
IServerStreamWriter<WeatherResponse> responseStream,
ServerCallContext context)
{
_logger.LogInformation("Starting weather stream for {Location}", request.Location);
// Simulate sending 5 forecasts at 1-second intervals
for (int i = 0; i < 5 && !context.CancellationToken.IsCancellationRequested; i++)
{
WeatherResponse forecast = new WeatherResponse
{
Location = request.Location,
TemperatureCelsius = 20 + i * 1.5, // Temperature increases each day
Condition = i % 2 == 0 ? "Sunny" : "Partly Cloudy",
Humidity = 60 - i * 2,
WindSpeed = 10 + i
};
await responseStream.WriteAsync(forecast);
await Task.Delay(1000); // Wait 1 second between forecasts
}
}
// Implement client streaming
public override async Task<UpdateSummary> SendWeatherUpdates(
IAsyncStreamReader<WeatherData> requestStream,
ServerCallContext context)
{
int updateCount = 0;
try
{
while (await requestStream.MoveNext())
{
WeatherData update = requestStream.Current;
_logger.LogInformation("Received weather update for {Location}: {Temperature}°C, {Condition}",
update.Location, update.TemperatureCelsius, update.Condition);
updateCount++;
// Process the update (in a real app, you might save to a database)
}
return new UpdateSummary
{
UpdatesReceived = updateCount,
Success = true,
Message = $"Successfully processed {updateCount} weather updates."
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing weather updates");
return new UpdateSummary
{
UpdatesReceived = updateCount,
Success = false,
Message = $"Error after processing {updateCount} updates: {ex.Message}"
};
}
}
// Implement bidirectional streaming
public override async Task WeatherChat(
IAsyncStreamReader<WeatherRequest> requestStream,
IServerStreamWriter<WeatherResponse> responseStream,
ServerCallContext context)
{
// Read requests and respond to each one
while (await requestStream.MoveNext() && !context.CancellationToken.IsCancellationRequested)
{
WeatherRequest request = requestStream.Current;
_logger.LogInformation("Weather chat request for {Location}", request.Location);
// Generate a response for this specific location
WeatherResponse response = new WeatherResponse
{
Location = request.Location,
TemperatureCelsius = new Random().Next(0, 35),
Condition = GetRandomCondition(),
Humidity = new Random().Next(30, 90),
WindSpeed = new Random().Next(0, 30)
};
await responseStream.WriteAsync(response);
}
}
private string GetRandomCondition()
{
string[] conditions = { "Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Stormy", "Snowy" };
return conditions[new Random().Next(conditions.Length)];
}
}
}
Configuring the ASP.NET Core Application
Set up the necessary services and middleware in Program.cs
:
using GrpcService;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = true;
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16 MB
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16 MB
});
WebApplication app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<WeatherService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client.");
});
});
app.Run();
Creating a gRPC Client
Let’s create a client to consume our gRPC service. First, create a new console application:
dotnet new console -o GrpcClient
cd GrpcClient
Add the required NuGet packages:
dotnet add package Google.Protobuf
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Tools
Copy or reference your .proto
file and configure it in your .csproj
:
<ItemGroup>
<Protobuf Include="..\GrpcService\Protos\weather.proto" GrpcServices="Client" />
</ItemGroup>
Implement a client to call the gRPC service:
using System;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using GrpcService;
namespace GrpcClient
{
class Program
{
static async Task Main(string[] args)
{
// Create a channel
using GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001");
// Create client
Weather.WeatherService.WeatherServiceClient client = new Weather.WeatherService.WeatherServiceClient(channel);
// Call unary method
await CallUnaryMethod(client);
// Call server streaming method
await CallServerStreamingMethod(client);
// Call client streaming method
await CallClientStreamingMethod(client);
// Call bidirectional streaming method
await CallBidirectionalStreamingMethod(client);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
private static async Task CallUnaryMethod(Weather.WeatherService.WeatherServiceClient client)
{
Console.WriteLine("=== UNARY CALL ===");
try
{
WeatherResponse response = await client.GetCurrentWeatherAsync(
new WeatherRequest { Location = "Seattle", IncludeForecast = false });
Console.WriteLine($"Current weather in {response.Location}:");
Console.WriteLine($"Temperature: {response.TemperatureCelsius}°C");
Console.WriteLine($"Condition: {response.Condition}");
Console.WriteLine($"Humidity: {response.Humidity}%");
Console.WriteLine($"Wind Speed: {response.WindSpeed} km/h");
}
catch (RpcException ex)
{
Console.WriteLine($"RPC failed: {ex.Status.Detail}");
}
Console.WriteLine();
}
private static async Task CallServerStreamingMethod(Weather.WeatherService.WeatherServiceClient client)
{
Console.WriteLine("=== SERVER STREAMING CALL ===");
try
{
WeatherRequest request = new WeatherRequest { Location = "Seattle", IncludeForecast = true };
using AsyncServerStreamingCall<WeatherResponse> call = client.GetWeatherStream(request);
int forecastDay = 1;
await foreach (WeatherResponse forecast in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Day {forecastDay} forecast for {forecast.Location}:");
Console.WriteLine($"Temperature: {forecast.TemperatureCelsius}°C");
Console.WriteLine($"Condition: {forecast.Condition}");
Console.WriteLine($"Humidity: {forecast.Humidity}%");
Console.WriteLine($"Wind Speed: {forecast.WindSpeed} km/h");
Console.WriteLine();
forecastDay++;
}
}
catch (RpcException ex)
{
Console.WriteLine($"RPC failed: {ex.Status.Detail}");
}
Console.WriteLine();
}
private static async Task CallClientStreamingMethod(Weather.WeatherService.WeatherServiceClient client)
{
Console.WriteLine("=== CLIENT STREAMING CALL ===");
try
{
using AsyncClientStreamingCall<WeatherData, UpdateSummary> call = client.SendWeatherUpdates();
// Send several weather updates
for (int i = 0; i < 5; i++)
{
WeatherData update = new WeatherData
{
Location = "Seattle",
TemperatureCelsius = 20 + i,
Condition = i % 2 == 0 ? "Sunny" : "Cloudy",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
Console.WriteLine($"Sending update: {update.TemperatureCelsius}°C, {update.Condition}");
await call.RequestStream.WriteAsync(update);
await Task.Delay(500); // Wait half a second between updates
}
// Complete the request
await call.RequestStream.CompleteAsync();
// Get the summary response
UpdateSummary summary = await call.ResponseAsync;
Console.WriteLine($"Update summary: {summary.Message}");
Console.WriteLine($"Updates received: {summary.UpdatesReceived}, Success: {summary.Success}");
}
catch (RpcException ex)
{
Console.WriteLine($"RPC failed: {ex.Status.Detail}");
}
Console.WriteLine();
}
private static async Task CallBidirectionalStreamingMethod(Weather.WeatherService.WeatherServiceClient client)
{
Console.WriteLine("=== BIDIRECTIONAL STREAMING CALL ===");
try
{
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using AsyncDuplexStreamingCall<WeatherRequest, WeatherResponse> call = client.WeatherChat(cancellationToken: cts.Token);
// Start task to read responses
Task readTask = Task.Run(async () =>
{
try
{
await foreach (WeatherResponse response in call.ResponseStream.ReadAllAsync(cts.Token))
{
Console.WriteLine($"Received weather for {response.Location}:");
Console.WriteLine($"Temperature: {response.TemperatureCelsius}°C");
Console.WriteLine($"Condition: {response.Condition}");
Console.WriteLine();
}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
{
Console.WriteLine("Stream cancelled.");
}
});
// Send requests for different cities
string[] cities = { "Seattle", "New York", "San Francisco", "London", "Tokyo" };
foreach (string city in cities)
{
if (cts.Token.IsCancellationRequested)
break;
WeatherRequest request = new WeatherRequest { Location = city, IncludeForecast = false };
Console.WriteLine($"Requesting weather for {city}...");
await call.RequestStream.WriteAsync(request);
await Task.Delay(1000, cts.Token); // Wait a second between requests
}
// Complete sending
await call.RequestStream.CompleteAsync();
// Wait for all responses
await readTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled.");
}
catch (RpcException ex)
{
Console.WriteLine($"RPC failed: {ex.Status.Detail}");
}
Console.WriteLine();
}
}
}
Advanced gRPC Features
Authentication and Authorization
SSL/TLS
gRPC services typically use Transport Layer Security (TLS) for securing communication.
JWT Authentication
Implement JWT authentication in your gRPC service:
// 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(options =>
{
options.AddPolicy("WeatherPolicy", policy =>
{
policy.RequireClaim("scope", "weather.read");
});
});
app.UseAuthentication();
app.UseAuthorization();
In your service, apply authorization:
[Authorize(Policy = "WeatherPolicy")]
public override Task<WeatherResponse> GetCurrentWeather(
WeatherRequest request,
ServerCallContext context)
{
string userId = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("User {UserId} is accessing weather data for {Location}", userId, request.Location);
// Rest of the implementation...
}
On the client side, add authentication:
// Create a channel with credentials
string token = "your-jwt-token";
CallOptions callOptions = new CallOptions(new Metadata
{
{ "Authorization", $"Bearer {token}" }
});
// Use the options when making calls
WeatherResponse response = await client.GetCurrentWeatherAsync(
new WeatherRequest { Location = "Seattle" },
callOptions);
Error Handling
gRPC has standardized error codes that can be used to communicate errors:
public override Task<WeatherResponse> GetCurrentWeather(
WeatherRequest request,
ServerCallContext context)
{
if (string.IsNullOrEmpty(request.Location))
{
throw new RpcException(new Status(
StatusCode.InvalidArgument, "Location cannot be empty"));
}
try
{
// Fetch weather data
// ...
}
catch (Exception ex)
{
throw new RpcException(new Status(
StatusCode.Internal, $"An internal error occurred: {ex.Message}"));
}
// Rest of the implementation...
}
Client-side error handling:
try
{
WeatherResponse response = await client.GetCurrentWeatherAsync(
new WeatherRequest { Location = "Seattle" });
// Process response...
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
Console.WriteLine($"Invalid argument: {ex.Status.Detail}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Internal)
{
Console.WriteLine($"Server error: {ex.Status.Detail}");
}
catch (RpcException ex)
{
Console.WriteLine($"RPC failed: {ex.Status.Code} - {ex.Status.Detail}");
}
Interceptors
Interceptors allow you to add cross-cutting concerns like logging, metrics, and authentication:
// Server-side interceptor
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)
{
string methodName = context.Method;
_logger.LogInformation("Starting call to {Method}", methodName);
Stopwatch stopwatch = Stopwatch.StartNew();
try
{
return await continuation(request, context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in {Method}", methodName);
throw;
}
finally
{
stopwatch.Stop();
_logger.LogInformation("Call to {Method} completed in {ElapsedMilliseconds}ms",
methodName, stopwatch.ElapsedMilliseconds);
}
}
}
// Register the interceptor
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
Load Balancing
Configure client-side load balancing:
// Using DNS for service discovery
DnsEndPoint[] endpoints = new[]
{
new DnsEndPoint("service1.example.com", 443),
new DnsEndPoint("service2.example.com", 443)
};
LoadBalancerOptions loadBalancerOptions = new LoadBalancerOptions
{
ServiceConfig = new ServiceConfig
{
LoadBalancingConfigs = { new RoundRobinConfig() }
}
};
GrpcChannel channel = GrpcChannel.ForAddress("dns:///example.com", new GrpcChannelOptions
{
Credentials = ChannelCredentials.SecureSsl,
ServiceConfig = loadBalancerOptions.ServiceConfig
});
Deadline/Timeout
Set deadlines to limit how long a gRPC call can take:
// Client-side deadline
DateTime deadline = DateTime.UtcNow.AddSeconds(5);
CallOptions options = new CallOptions(deadline: deadline);
try
{
WeatherResponse response = await client.GetCurrentWeatherAsync(
new WeatherRequest { Location = "Seattle" },
options);
// Process response...
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("Request timed out");
}
Health Checking
Implement health checks in your service:
// Add health checks service
builder.Services.AddGrpcHealthChecks()
.AddCheck("Database", () =>
{
// Check database connectivity
bool isHealthy = CheckDatabaseConnection();
return isHealthy
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Database connection failed");
})
.AddCheck("Weather API", () =>
{
// Check external API dependency
bool isHealthy = CheckExternalWeatherApi();
return isHealthy
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Weather API is unavailable");
});
// Map the health checks service
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<WeatherService>();
endpoints.MapGrpcHealthChecksService();
});
On the client side, check service health:
using Grpc.Health.V1;
// Create health client
Health.HealthClient healthClient = new Health.HealthClient(channel);
// Check overall service health
HealthCheckResponse response = await healthClient.CheckAsync(
new HealthCheckRequest { Service = "" });
if (response.Status == HealthCheckResponse.Types.ServingStatus.Serving)
{
Console.WriteLine("Service is healthy");
}
else
{
Console.WriteLine($"Service is unhealthy: {response.Status}");
}
// Check specific service component
response = await healthClient.CheckAsync(
new HealthCheckRequest { Service = "Database" });
Console.WriteLine($"Database health: {response.Status}");
Performance Optimization
Message Size Optimization
Protocol Buffers are already optimized for size, but follow these additional practices:
- Use appropriate data types (e.g., int32 vs int64)
- Reuse message types when possible
- Use optional fields when appropriate
- Consider using packed repeated fields for numeric types
message OptimizedMessage {
// Use appropriate sizes
int32 small_number = 1;
int64 large_number = 2;
// Use packed repeated fields for numeric values
repeated int32 values = 3 [packed=true];
// Use enums instead of strings when possible
enum Status {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
}
Status status = 4;
// Use optional for fields that may not be set
optional string notes = 5;
}
Connection Management
For better performance, reuse channels:
// Create a singleton channel for the application lifetime
public class GrpcClientFactory
{
private static readonly Lazy<GrpcChannel> _channel = new Lazy<GrpcChannel>(() =>
GrpcChannel.ForAddress("https://localhost:5001"));
public static GrpcChannel Channel => _channel.Value;
public static Weather.WeatherService.WeatherServiceClient CreateWeatherClient()
{
return new Weather.WeatherService.WeatherServiceClient(Channel);
}
}
Async Processing
Use asynchronous processing for better throughput:
public class WeatherService : Weather.WeatherService.WeatherServiceBase
{
private readonly IWeatherRepository _repository;
public WeatherService(IWeatherRepository repository)
{
_repository = repository;
}
public override async Task<WeatherResponse> GetCurrentWeather(
WeatherRequest request,
ServerCallContext context)
{
// Asynchronously fetch weather data
WeatherData data = await _repository.GetCurrentWeatherAsync(
request.Location,
context.CancellationToken);
return new WeatherResponse
{
Location = data.Location,
TemperatureCelsius = data.Temperature,
Condition = data.Condition,
Humidity = data.Humidity,
WindSpeed = data.WindSpeed
};
}
}
Integration with ASP.NET Core
Combining gRPC and REST APIs
You can combine gRPC services with REST APIs in the same ASP.NET Core application:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddGrpc();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure CORS if needed
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
WebApplication app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("AllowAll");
app.UseEndpoints(endpoints =>
{
// Map gRPC services
endpoints.MapGrpcService<WeatherService>();
// Map REST controllers
endpoints.MapControllers();
// Map minimal APIs
endpoints.MapGet("/api/health", () => Results.Ok(new { status = "healthy" }));
});
app.Run();
Using gRPC-Web for Browser Clients
gRPC-Web allows you to use gRPC from browsers:
// Configure gRPC-Web in Program.cs
builder.Services.AddGrpc();
builder.Services.AddGrpcWeb(options => options.GrpcWebEnabled = true);
// Add CORS for browser access
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
});
});
WebApplication app = builder.Build();
app.UseRouting();
app.UseCors("AllowAll");
app.UseGrpcWeb(); // Add gRPC-Web middleware
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<WeatherService>()
.EnableGrpcWeb() // Enable gRPC-Web for this service
.RequireCors("AllowAll");
});
Testing gRPC Services
Unit Testing
Test your gRPC service logic with unit tests:
using Microsoft.Extensions.Logging;
using Moq;
using System.Threading.Tasks;
using Xunit;
namespace GrpcService.Tests
{
public class WeatherServiceTests
{
[Fact]
public async Task GetCurrentWeather_ReturnsWeatherData()
{
// Arrange
Mock<ILogger<WeatherService>> loggerMock = new Mock<ILogger<WeatherService>>();
WeatherService service = new WeatherService(loggerMock.Object);
WeatherRequest request = new WeatherRequest
{
Location = "Seattle",
IncludeForecast = false
};
ServerCallContext context = TestServerCallContext.Create();
// Act
WeatherResponse response = await service.GetCurrentWeather(request, context);
// Assert
Assert.NotNull(response);
Assert.Equal("Seattle", response.Location);
Assert.True(response.TemperatureCelsius > -50 && response.TemperatureCelsius < 50);
Assert.NotEmpty(response.Condition);
}
}
// Helper class for creating test ServerCallContext
public static class TestServerCallContext
{
public static ServerCallContext Create(CancellationToken cancellationToken = default)
{
return new TestContext(cancellationToken);
}
private class TestContext : ServerCallContext
{
private readonly CancellationToken _cancellationToken;
public TestContext(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
}
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
return Task.CompletedTask;
}
protected override ContextPropagationToken CreatePropagationTokenCore(
ContextPropagationOptions options)
{
throw new NotImplementedException();
}
protected override string MethodCore => "TestMethod";
protected override string HostCore => "localhost";
protected override string PeerCore => "peer";
protected override DateTime DeadlineCore { get; }
protected override Metadata RequestHeadersCore { get; } = new Metadata();
protected override CancellationToken CancellationTokenCore => _cancellationToken;
protected override Metadata ResponseTrailersCore { get; } = new Metadata();
protected override Status StatusCore { get; set; }
protected override WriteOptions WriteOptionsCore { get; set; }
protected override AuthContext AuthContextCore { get; }
}
}
}
Integration Testing
Create integration tests using test server:
using Grpc.Net.Client;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using Xunit;
namespace GrpcService.Tests
{
public class WeatherServiceIntegrationTests
{
[Fact]
public async Task GetCurrentWeather_ReturnsSuccessfulResponse()
{
// Arrange
IHost host = await CreateHostWithTestServer();
GrpcChannel channel = GrpcChannel.ForAddress(
"http://localhost",
new GrpcChannelOptions
{
HttpHandler = host.GetTestServer().CreateHandler()
});
Weather.WeatherService.WeatherServiceClient client =
new Weather.WeatherService.WeatherServiceClient(channel);
// Act
WeatherResponse response = await client.GetCurrentWeatherAsync(
new WeatherRequest { Location = "Seattle" });
// Assert
Assert.NotNull(response);
Assert.Equal("Seattle", response.Location);
Assert.NotEmpty(response.Condition);
await host.StopAsync();
}
private static async Task<IHost> CreateHostWithTestServer()
{
IHost host = await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.UseStartup<Startup>();
})
.StartAsync();
return host;
}
}
}
Comparing gRPC with Other REST Alternatives
Feature | gRPC | GraphQL | SOAP | OData | REST |
---|---|---|---|---|---|
Transport | HTTP/2 | HTTP | HTTP, SMTP, etc. | HTTP | HTTP |
Encoding | Protocol Buffers | JSON/XML | XML | JSON/XML | Any (typically JSON) |
Contract | .proto files | Schema | WSDL/XSD | Metadata | Optional (OpenAPI) |
Types | Strong | Strong | Strong | Strong | Loose |
Streaming | Full | Subscriptions | No | No | No (WebSockets) |
Learning curve | Medium-High | Medium-High | High | Medium | Low |
Payload size | Small (binary) | Medium (JSON) | Large (XML) | Medium (JSON) | Medium (JSON) |
Browser support | Limited | Full | Limited | Full | Full |
Code generation | Built-in | Optional | Built-in | Optional | Optional |
Performance | Excellent | Good | Poor | Good | Good |
Best Practices for gRPC API Development
- Define Clear Service Boundaries - Follow Single Responsibility Principle
- Use Streaming Appropriately - Use unary calls for simple requests and streaming for real-time data
- Design for Performance - Optimize message size and minimize network calls
- Include Proper Error Handling - Use appropriate status codes and provide meaningful error messages
- Implement Timeouts - Set reasonable deadlines for all operations
- Add Health Checks - Implement health checking for service discovery and monitoring
- Configure TLS - Secure your gRPC services with TLS
- Optimize Message Serialization - Design Protocol Buffer messages for efficient serialization
- Use Interceptors - Implement cross-cutting concerns with interceptors
- Implement Proper Load Balancing - Configure load balancing for production deployments
Conclusion
gRPC is a powerful alternative to REST APIs for building high-performance, strongly-typed services. Its efficiency, performance, and built-in support for various communication patterns make it an excellent choice for microservice architectures and scenarios where network efficiency is crucial.
While gRPC has a steeper learning curve and limited browser support compared to REST, its advantages in performance, strong typing, and code generation often outweigh these limitations for backend-to-backend communication.
The .NET ecosystem provides excellent support for building gRPC services with ASP.NET Core, making it a compelling choice for modern distributed systems.