gRPC

Understanding gRPC as an alternative to REST APIs for high-performance services in .NET

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

FeaturegRPCREST
ProtocolHTTP/2HTTP (1.1, 2, 3)
Payload formatProtocol Buffers (binary)Typically JSON (text)
ContractRequired (.proto files)Optional (OpenAPI)
Code generationYes, built-inOptional with third-party tools
StreamingBidirectional native streamingLimited (with WebSockets or SSE)
Browser supportLimited (requires gRPC-Web)Native
Payload sizeSmallerLarger
Human readabilityNoYes
Learning curveSteeperGentler

When to Use gRPC Instead of REST

gRPC is particularly well-suited for:

  1. Microservice architectures - For service-to-service communication where performance is critical
  2. Low-latency, high-throughput communication - When you need efficient binary serialization
  3. Polyglot environments - When you need to generate consistent client/server stubs across multiple languages
  4. Bidirectional streaming - When you need real-time communication with streaming support
  5. Constrained environments - Mobile applications or IoT devices where bandwidth is limited
  6. 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:

  1. Use appropriate data types (e.g., int32 vs int64)
  2. Reuse message types when possible
  3. Use optional fields when appropriate
  4. 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

FeaturegRPCGraphQLSOAPODataREST
TransportHTTP/2HTTPHTTP, SMTP, etc.HTTPHTTP
EncodingProtocol BuffersJSON/XMLXMLJSON/XMLAny (typically JSON)
Contract.proto filesSchemaWSDL/XSDMetadataOptional (OpenAPI)
TypesStrongStrongStrongStrongLoose
StreamingFullSubscriptionsNoNoNo (WebSockets)
Learning curveMedium-HighMedium-HighHighMediumLow
Payload sizeSmall (binary)Medium (JSON)Large (XML)Medium (JSON)Medium (JSON)
Browser supportLimitedFullLimitedFullFull
Code generationBuilt-inOptionalBuilt-inOptionalOptional
PerformanceExcellentGoodPoorGoodGood

Best Practices for gRPC API Development

  1. Define Clear Service Boundaries - Follow Single Responsibility Principle
  2. Use Streaming Appropriately - Use unary calls for simple requests and streaming for real-time data
  3. Design for Performance - Optimize message size and minimize network calls
  4. Include Proper Error Handling - Use appropriate status codes and provide meaningful error messages
  5. Implement Timeouts - Set reasonable deadlines for all operations
  6. Add Health Checks - Implement health checking for service discovery and monitoring
  7. Configure TLS - Secure your gRPC services with TLS
  8. Optimize Message Serialization - Design Protocol Buffer messages for efficient serialization
  9. Use Interceptors - Implement cross-cutting concerns with interceptors
  10. 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.

Additional Resources