gRPC in ConnectSoft Microservice Template¶
Purpose & Overview¶
gRPC (gRPC Remote Procedure Calls) is a high-performance, language-agnostic RPC framework that enables efficient communication between microservices. In the ConnectSoft Microservice Template, gRPC serves as a primary communication protocol for inter-service communication, offering better performance than REST for service-to-service calls.
gRPC provides:
- HTTP/2 Based: Multiplexing, header compression, and binary protocol
- Protocol Buffers: Efficient binary serialization format
- Streaming: Bidirectional streaming for real-time communication
- Language Agnostic: Works across different programming languages
- Strongly Typed: Code generation from
.protodefinitions - Performance: Lower latency and higher throughput than REST/JSON
- Built-in Features: Authentication, interceptors, health checks, reflection
gRPC Philosophy
gRPC is designed for efficient service-to-service communication. It uses Protocol Buffers (protobuf) for serialization, HTTP/2 for transport, and generates strongly-typed client/server code from .proto definitions. This makes it ideal for microservices architectures where performance and type safety are critical.
Architecture Overview¶
gRPC in ConnectSoft Architecture¶
Client Applications / Other Services
↓ (gRPC Call)
API Layer (ServiceModel.Grpc)
├── gRPC Services (Proto-generated)
├── Service Implementations
└── gRPC Interceptors (Logging, Validation, Error Handling)
↓ (Uses Domain Services)
Application Layer (DomainModel)
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓ (Uses Repositories)
Infrastructure Layer
└── Persistence Model
Key Integration Points¶
| Layer | Component | Responsibility |
|---|---|---|
| ServiceModel.Grpc | .proto files, generated code |
Service contracts and DTOs |
| ServiceModel.Grpc | Service implementations | gRPC service handlers |
| ApplicationModel | GrpcExtensions | gRPC server configuration |
| DomainModel | Processors, Retrievers | Business logic execution |
| PersistenceModel | Repositories | Data access |
Protocol Buffer Definitions¶
Proto File Structure¶
gRPC services are defined using Protocol Buffer (.proto) files:
// MicroserviceAggregateRootsService.proto
syntax = "proto3";
package connectsoft.microservicetemplate;
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option csharp_namespace = "ConnectSoft.MicroserviceTemplate.ServiceModel.Grpc";
// Service Definition
service MicroserviceAggregateRootsService {
// Unary RPC (single request, single response)
rpc GetMicroserviceAggregateRoot (GetMicroserviceAggregateRootRequest)
returns (GetMicroserviceAggregateRootResponse);
rpc CreateMicroserviceAggregateRoot (CreateMicroserviceAggregateRootRequest)
returns (CreateMicroserviceAggregateRootResponse);
rpc DeleteMicroserviceAggregateRoot (DeleteMicroserviceAggregateRootRequest)
returns (DeleteMicroserviceAggregateRootResponse);
// Server Streaming (single request, stream of responses)
rpc StreamMicroserviceAggregateRoots (StreamMicroserviceAggregateRootsRequest)
returns (stream MicroserviceAggregateRootResponse);
// Client Streaming (stream of requests, single response)
rpc CreateMicroserviceAggregateRoots (stream CreateMicroserviceAggregateRootRequest)
returns (CreateMicroserviceAggregateRootsResponse);
// Bidirectional Streaming (stream of requests, stream of responses)
rpc ChatMicroserviceAggregateRoots (stream ChatMessageRequest)
returns (stream ChatMessageResponse);
}
// Request/Response Messages
message GetMicroserviceAggregateRootRequest {
string object_id = 1; // Field numbers must be unique
}
message GetMicroserviceAggregateRootResponse {
string object_id = 1;
string some_value = 2;
google.protobuf.Timestamp created_on = 3;
}
message CreateMicroserviceAggregateRootRequest {
string object_id = 1;
string some_value = 2;
}
message CreateMicroserviceAggregateRootResponse {
string object_id = 1;
bool success = 2;
string message = 3;
}
Proto Best Practices¶
- Field Numbers: Use field numbers 1-15 for frequently used fields (1 byte encoding)
- Naming Convention: Use
snake_casefor field names,PascalCasefor message types - Package Names: Use reverse domain notation:
connectsoft.microservicetemplate - Versioning: Use package versions for breaking changes:
connectsoft.microservicetemplate.v2 - Deprecation: Use
deprecated = truefor obsolete fields/methods
message Order {
reserved 4, 5; // Reserve field numbers for deleted fields
reserved "old_field_name"; // Reserve field names
string id = 1;
string customer_id = 2;
int32 quantity = 3;
// Deprecated field
string old_field = 6 [deprecated = true];
}
Service Implementation¶
gRPC Service Class¶
gRPC services inherit from the generated base class:
// MicroserviceAggregateRootsService.cs
namespace ConnectSoft.MicroserviceTemplate.ServiceModel.Grpc
{
using System;
using System.Threading.Tasks;
using ConnectSoft.MicroserviceTemplate.DomainModel;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.Extensions.Logging;
/// <summary>
/// gRPC service implementation for MicroserviceAggregateRoots operations.
/// </summary>
public class MicroserviceAggregateRootsService :
MicroserviceAggregateRootsServiceBase
{
private readonly ILogger<MicroserviceAggregateRootsService> logger;
private readonly IMicroserviceAggregateRootsRetriever retriever;
private readonly IMicroserviceAggregateRootsProcessor processor;
public MicroserviceAggregateRootsService(
ILogger<MicroserviceAggregateRootsService> logger,
IMicroserviceAggregateRootsRetriever retriever,
IMicroserviceAggregateRootsProcessor processor)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.retriever = retriever ?? throw new ArgumentNullException(nameof(retriever));
this.processor = processor ?? throw new ArgumentNullException(nameof(processor));
}
/// <summary>
/// Get a microservice aggregate root by ID.
/// </summary>
public override async Task<GetMicroserviceAggregateRootResponse> GetMicroserviceAggregateRoot(
GetMicroserviceAggregateRootRequest request,
ServerCallContext context)
{
this.logger.LogInformation(
"GetMicroserviceAggregateRoot called with ObjectId: {ObjectId}",
request.ObjectId);
try
{
// Validate request
if (string.IsNullOrEmpty(request.ObjectId))
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, "ObjectId is required"));
}
if (!Guid.TryParse(request.ObjectId, out var objectId))
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, "Invalid ObjectId format"));
}
// Call domain service
var input = new GetMicroserviceAggregateRootDetailsInput
{
ObjectId = objectId
};
var aggregate = await this.retriever
.GetMicroserviceAggregateRootDetails(input, context.CancellationToken)
.ConfigureAwait(false);
if (aggregate == null)
{
throw new RpcException(
new Status(StatusCode.NotFound, $"Aggregate with ID {request.ObjectId} not found"));
}
// Map to response
var response = new GetMicroserviceAggregateRootResponse
{
ObjectId = aggregate.ObjectId.ToString(),
SomeValue = aggregate.SomeValue ?? string.Empty,
CreatedOn = aggregate.CreatedOn.ToTimestamp()
};
return response;
}
catch (RpcException)
{
// Re-throw RPC exceptions
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error getting microservice aggregate root");
throw new RpcException(
new Status(StatusCode.Internal, "Internal server error"));
}
}
/// <summary>
/// Create a new microservice aggregate root.
/// </summary>
public override async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
CreateMicroserviceAggregateRootRequest request,
ServerCallContext context)
{
this.logger.LogInformation(
"CreateMicroserviceAggregateRoot called with ObjectId: {ObjectId}",
request.ObjectId);
try
{
// Validate request
if (string.IsNullOrEmpty(request.ObjectId))
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, "ObjectId is required"));
}
if (!Guid.TryParse(request.ObjectId, out var objectId))
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, "Invalid ObjectId format"));
}
// Call domain service
var input = new CreateMicroserviceAggregateRootInput
{
ObjectId = objectId,
SomeValue = request.SomeValue
};
var aggregate = await this.processor
.CreateMicroserviceAggregateRoot(input, context.CancellationToken)
.ConfigureAwait(false);
// Map to response
var response = new CreateMicroserviceAggregateRootResponse
{
ObjectId = aggregate.ObjectId.ToString(),
Success = true,
Message = "Aggregate created successfully"
};
return response;
}
catch (RpcException)
{
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error creating microservice aggregate root");
throw new RpcException(
new Status(StatusCode.Internal, "Internal server error"));
}
}
}
}
Configuration¶
Service Registration¶
gRPC services are registered in the application startup:
// MicroserviceRegistrationExtensions.cs
public static IServiceCollection ConfigureMicroserviceServices(
this IServiceCollection services,
IConfiguration configuration,
IWebHostEnvironment environment)
{
// ... other service registrations ...
#if UseGrpc
services.AddGrpcCommunication();
#endif
return services;
}
gRPC Extension Method¶
The AddGrpcCommunication() method configures all gRPC services:
// GrpcExtensions.cs
internal static IServiceCollection AddGrpcCommunication(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// Register gRPC services
services.AddGrpc(options =>
{
// Enable detailed error messages (development only)
options.EnableDetailedErrors = false;
// Maximum receive message size (4 MB default)
options.MaxReceiveMessageSize = 4 * 1024 * 1024;
// Maximum send message size (4 MB default)
options.MaxSendMessageSize = 4 * 1024 * 1024;
// Compression providers
options.CompressionProviders = new List<ICompressionProvider>
{
new GzipCompressionProvider(CompressionLevel.Fastest)
};
// Response compression
options.ResponseCompressionAlgorithm = "gzip";
options.ResponseCompressionLevel = CompressionLevel.Fastest;
// Interceptors
options.Interceptors.Add<GrpcServerLoggingInterceptor>();
options.Interceptors.Add<GrpcValidationInterceptor>();
options.Interceptors.Add<GrpcExceptionInterceptor>();
});
// Register gRPC service implementations
services.AddGrpcReflection(); // For gRPC reflection API
return services;
}
Endpoint Mapping¶
gRPC endpoints are mapped in the middleware pipeline:
// MicroserviceRegistrationExtensions.cs - UseMicroserviceServices
internal static IApplicationBuilder UseMicroserviceServices(
this IApplicationBuilder application,
IConfiguration configuration,
IWebHostEnvironment environment,
ILoggerFactory loggerFactory,
IHostApplicationLifetime hostApplicationLifetime)
{
// ... other middleware ...
application.UseRouting();
application.UseEndpoints(endpoints =>
{
#if UseGrpc
endpoints.MapGrpcService<MicroserviceAggregateRootsService>();
endpoints.MapGrpcService<FeatureAService>();
// gRPC reflection (development only)
if (environment.IsDevelopment())
{
endpoints.MapGrpcReflectionService();
}
#endif
// ... other endpoints ...
});
return application;
}
appsettings.json Configuration¶
{
"Kestrel": {
"Endpoints": {
"Grpc": {
"Url": "https://localhost:5001",
"Protocols": "Http2"
},
"Http": {
"Url": "http://localhost:5000",
"Protocols": "Http1"
}
}
},
"Grpc": {
"EnableDetailedErrors": false,
"MaxReceiveMessageSize": 4194304,
"MaxSendMessageSize": 4194304,
"ResponseCompressionAlgorithm": "gzip",
"ResponseCompressionLevel": "Optimal"
}
}
gRPC Compression¶
gRPC supports response compression to reduce bandwidth usage and improve performance. gRPC compression is separate from HTTP response compression and uses different mechanisms:
- HTTP Response Compression: Uses
Content-EncodingandAccept-Encodingheaders, configured viaCompressionOptions - gRPC Compression: Uses
grpc-encodingandgrpc-accept-encodingheaders, configured viaGrpcOptions
Both can be enabled simultaneously for different protocols.
Compression Providers¶
The template supports the following compression providers:
- Gzip (
GzipCompressionProvider) - Built-in provider fromGrpc.Net.Compression, available by default, widely supported - Brotli (
BrotliCompressionProvider) - Custom implementation in the template (see ASP.NET Core 6: Bring Your Custom Compression Provider in gRPC), provides better compression ratio than Gzip but requires custom implementation as it's not included inGrpc.Net.Compression
Configuration¶
gRPC compression is configured in the Grpc section of appsettings.json and requires both the Compression and UseGrpc feature flags to be enabled:
{
"Grpc": {
"ResponseCompressionAlgorithm": "gzip", // "gzip", "br", "deflate", or null to disable
"ResponseCompressionLevel": "Optimal" // Fastest, Optimal, SmallestSize, NoCompression
}
}
Configuration Parameters:
ResponseCompressionAlgorithm(string?, optional) - The compression algorithm to use. Supported values:"gzip"- Gzip compression (default, widely supported)"br"- Brotli compression (better compression ratio)"deflate"- Deflate compression-
null- Disable compression (default if not specified) -
ResponseCompressionLevel(CompressionLevel?, optional) - The compression level to use: Fastest- Fastest compression, lower ratioOptimal- Balanced compression (recommended default)SmallestSize- Maximum compression, higher CPU usageNoCompression- No compression
How It Works¶
-
Client Request: The client sends a
grpc-accept-encodingheader indicating supported compression algorithms (e.g.,grpc-accept-encoding: gzip, br) -
Server Response: If compression is enabled and the client supports it, the server:
- Compresses the response using the configured algorithm
- Sets the
grpc-encodingheader to indicate the compression used -
Sends the compressed response
-
Client Decompression: The client automatically decompresses the response based on the
grpc-encodingheader
Best Practices¶
- When to Enable: Enable compression for gRPC services that send large payloads or when bandwidth is a concern
- Algorithm Selection:
- Use
gzipfor maximum compatibility - Use
br(Brotli) for better compression ratios when client support is available - Compression Level:
- Use
Optimalfor most scenarios (balanced performance and compression) - Use
Fastestfor high-traffic scenarios - Use
SmallestSizefor low-traffic scenarios where bandwidth is critical - Performance Considerations: Compression adds CPU overhead. Monitor CPU usage and network bandwidth to find the optimal balance for your workload
- Protocol Buffers: Protocol buffers are already efficient (binary format), so compression benefits may be smaller than with JSON. Test with your specific payloads to determine if compression is beneficial
Example: Enabling Brotli Compression¶
References¶
gRPC Interceptors¶
Logging Interceptor¶
Logs all gRPC calls with request/response details:
// GrpcServerLoggingInterceptor.cs
public class GrpcServerLoggingInterceptor : Interceptor
{
private readonly ILogger<GrpcServerLoggingInterceptor> logger;
public GrpcServerLoggingInterceptor(ILogger<GrpcServerLoggingInterceptor> logger)
{
this.logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
using (this.logger.BeginScope(
new Dictionary<string, object>(StringComparer.Ordinal)
{
["GrpcMethod"] = context.Method,
["GrpcPeer"] = context.Peer,
}))
{
try
{
this.logger.LogInformation(
"gRPC call method {GrpcMethod} started.",
context.Method);
var response = await base.UnaryServerHandler(request, context, continuation)
.ConfigureAwait(false);
this.logger.LogInformation(
"gRPC call method {GrpcMethod} completed successfully.",
context.Method);
return response;
}
catch (Exception exception)
{
this.logger.LogError(
exception,
"Error occurred during gRPC call method {GrpcMethod}.",
context.Method);
throw;
}
}
}
}
Validation Interceptor¶
Validates requests before processing:
// GrpcValidationInterceptor.cs
public class GrpcValidationInterceptor : Interceptor
{
private readonly IValidatorFactory validatorFactory;
public GrpcValidationInterceptor(IValidatorFactory validatorFactory)
{
this.validatorFactory = validatorFactory;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var validator = this.validatorFactory.GetValidator<TRequest>();
if (validator != null)
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
throw new RpcException(
new Status(StatusCode.InvalidArgument,
string.Join("; ", validationResult.Errors.Select(e => e.ErrorMessage))));
}
}
return await base.UnaryServerHandler(request, context, continuation)
.ConfigureAwait(false);
}
}
Exception Interceptor¶
Handles exceptions and converts them to appropriate gRPC status codes:
// GrpcExceptionInterceptor.cs
public class GrpcExceptionInterceptor : Interceptor
{
private readonly ILogger<GrpcExceptionInterceptor> logger;
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
{
this.logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await base.UnaryServerHandler(request, context, continuation)
.ConfigureAwait(false);
}
catch (RpcException)
{
// Re-throw RPC exceptions
throw;
}
catch (NotFoundException ex)
{
throw new RpcException(
new Status(StatusCode.NotFound, ex.Message));
}
catch (ValidationException ex)
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, ex.Message));
}
catch (UnauthorizedException ex)
{
throw new RpcException(
new Status(StatusCode.PermissionDenied, ex.Message));
}
catch (Exception ex)
{
this.logger.LogError(ex, "Unexpected error in gRPC call");
throw new RpcException(
new Status(StatusCode.Internal, "An internal error occurred"));
}
}
}
Streaming¶
Server Streaming¶
Server streaming allows sending multiple responses for a single request:
public override async Task StreamMicroserviceAggregateRoots(
StreamMicroserviceAggregateRootsRequest request,
IServerStreamWriter<MicroserviceAggregateRootResponse> responseStream,
ServerCallContext context)
{
this.logger.LogInformation("Streaming microservice aggregate roots");
var input = new GetMicroserviceAggregateRootsInput
{
Status = request.Status
};
await foreach (var aggregate in this.retriever
.StreamMicroserviceAggregateRoots(input, context.CancellationToken))
{
if (context.CancellationToken.IsCancellationRequested)
{
break;
}
var response = new MicroserviceAggregateRootResponse
{
ObjectId = aggregate.ObjectId.ToString(),
SomeValue = aggregate.SomeValue ?? string.Empty
};
await responseStream.WriteAsync(response).ConfigureAwait(false);
}
}
Client Streaming¶
Client streaming allows receiving multiple requests and returning a single response:
public override async Task<CreateMicroserviceAggregateRootsResponse> CreateMicroserviceAggregateRoots(
IAsyncStreamReader<CreateMicroserviceAggregateRootRequest> requestStream,
ServerCallContext context)
{
var createdCount = 0;
var errors = new List<string>();
await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
{
try
{
var input = new CreateMicroserviceAggregateRootInput
{
ObjectId = Guid.Parse(request.ObjectId),
SomeValue = request.SomeValue
};
await this.processor
.CreateMicroserviceAggregateRoot(input, context.CancellationToken)
.ConfigureAwait(false);
createdCount++;
}
catch (Exception ex)
{
errors.Add($"Failed to create {request.ObjectId}: {ex.Message}");
}
}
return new CreateMicroserviceAggregateRootsResponse
{
CreatedCount = createdCount,
Errors = { errors }
};
}
Bidirectional Streaming¶
Bidirectional streaming allows sending and receiving streams simultaneously:
public override async Task ChatMicroserviceAggregateRoots(
IAsyncStreamReader<ChatMessageRequest> requestStream,
IServerStreamWriter<ChatMessageResponse> responseStream,
ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
{
// Process request
var response = new ChatMessageResponse
{
Message = $"Echo: {request.Message}",
Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
};
await responseStream.WriteAsync(response).ConfigureAwait(false);
}
}
Client Usage¶
Creating gRPC Clients¶
gRPC clients are created using the generated client code:
// Client configuration
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
Credentials = ChannelCredentials.SecureSsl,
MaxReceiveMessageSize = 4 * 1024 * 1024,
MaxSendMessageSize = 4 * 1024 * 1024,
CompressionProviders = new List<ICompressionProvider>
{
new GzipCompressionProvider(CompressionLevel.Fastest)
}
});
var client = new MicroserviceAggregateRootsService.MicroserviceAggregateRootsServiceClient(channel);
// Unary call
var request = new GetMicroserviceAggregateRootRequest
{
ObjectId = "123e4567-e89b-12d3-a456-426614174000"
};
var response = await client.GetMicroserviceAggregateRootAsync(request);
// Server streaming
using var call = client.StreamMicroserviceAggregateRoots(new StreamMicroserviceAggregateRootsRequest());
await foreach (var item in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Received: {item.ObjectId}");
}
Dependency Injection for Clients¶
Register gRPC clients in the DI container:
// Register gRPC client
services.AddGrpcClient<MicroserviceAggregateRootsService.MicroserviceAggregateRootsServiceClient>(options =>
{
options.Address = new Uri("https://localhost:5001");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator // Development only
});
Use in services:
public class MyService
{
private readonly MicroserviceAggregateRootsService.MicroserviceAggregateRootsServiceClient grpcClient;
public MyService(
MicroserviceAggregateRootsService.MicroserviceAggregateRootsServiceClient grpcClient)
{
this.grpcClient = grpcClient;
}
public async Task<GetMicroserviceAggregateRootResponse> GetAggregateAsync(string id)
{
var request = new GetMicroserviceAggregateRootRequest { ObjectId = id };
return await this.grpcClient.GetMicroserviceAggregateRootAsync(request);
}
}
Error Handling¶
gRPC Status Codes¶
gRPC uses status codes to represent errors:
| Status Code | Description | Use Case |
|---|---|---|
OK |
Success | Operation completed successfully |
CANCELLED |
Cancelled | Operation was cancelled |
INVALID_ARGUMENT |
Invalid argument | Client provided invalid input |
DEADLINE_EXCEEDED |
Deadline exceeded | Operation timed out |
NOT_FOUND |
Not found | Requested resource not found |
ALREADY_EXISTS |
Already exists | Resource already exists |
PERMISSION_DENIED |
Permission denied | Insufficient permissions |
UNAUTHENTICATED |
Unauthenticated | Missing or invalid authentication |
RESOURCE_EXHAUSTED |
Resource exhausted | Out of resources (quota, rate limit) |
FAILED_PRECONDITION |
Failed precondition | Operation rejected due to precondition |
ABORTED |
Aborted | Operation aborted |
OUT_OF_RANGE |
Out of range | Operation attempted past valid range |
UNIMPLEMENTED |
Unimplemented | Operation not implemented |
INTERNAL |
Internal error | Internal server error |
UNAVAILABLE |
Unavailable | Service unavailable |
DATA_LOSS |
Data loss | Unrecoverable data loss |
Rich Error Details¶
Use Google.Protobuf.WellKnownTypes.Status for detailed error information:
var status = new Google.Rpc.Status
{
Code = (int)StatusCode.InvalidArgument,
Message = "Validation failed",
Details =
{
Any.Pack(new Google.Rpc.BadRequest
{
FieldViolations =
{
new Google.Rpc.BadRequest.Types.FieldViolation
{
Field = "object_id",
Description = "ObjectId is required"
}
}
})
}
};
throw new RpcException(new Status(StatusCode.InvalidArgument), status.ToString());
Exception Mapping¶
Map domain exceptions to gRPC status codes:
catch (MicroserviceAggregateRootNotFoundException ex)
{
throw new RpcException(
new Status(StatusCode.NotFound, ex.Message));
}
catch (MicroserviceAggregateRootAlreadyExistsException ex)
{
throw new RpcException(
new Status(StatusCode.AlreadyExists, ex.Message));
}
catch (ValidationException ex)
{
throw new RpcException(
new Status(StatusCode.InvalidArgument, ex.Message));
}
See Exception Handling for detailed information.
Health Checks¶
gRPC Health Protocol¶
gRPC has a standard health checking protocol:
// health.proto (standard gRPC health check proto)
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
Implementing Health Checks¶
// HealthService.cs
public class HealthService : Health.HealthBase
{
private readonly IHealthCheckService healthCheckService;
public HealthService(IHealthCheckService healthCheckService)
{
this.healthCheckService = healthCheckService;
}
public override async Task<HealthCheckResponse> Check(
HealthCheckRequest request,
ServerCallContext context)
{
var healthResult = await this.healthCheckService.CheckHealthAsync(
check => check.Tags.Contains(request.Service),
context.CancellationToken);
return new HealthCheckResponse
{
Status = healthResult.Status == HealthStatus.Healthy
? HealthCheckResponse.Types.ServingStatus.Serving
: HealthCheckResponse.Types.ServingStatus.NotServing
};
}
}
Register Health Check Service¶
See Health Checks for detailed information.
Observability¶
Structured Logging¶
gRPC operations are logged with structured context:
using (this.logger.BeginScope(
new Dictionary<string, object>(StringComparer.Ordinal)
{
["GrpcMethod"] = context.Method,
["GrpcPeer"] = context.Peer,
["GrpcRequestId"] = context.RequestHeaders.GetValue("request-id"),
}))
{
this.logger.LogInformation("Processing gRPC request");
}
Distributed Tracing¶
gRPC integrates with OpenTelemetry for distributed tracing:
// OpenTelemetryExtensions.cs
#if UseGrpc
tracingBuilder.AddGrpcClientInstrumentation();
tracingBuilder.AddGrpcServerInstrumentation();
#endif
Traces automatically include: - Request/response metadata - Method names - Peer information - Duration - Status codes
See Logging for detailed observability information.
Metrics¶
gRPC metrics are available via OpenTelemetry: - Request rate - Response latency - Error rate - Message sizes - Active streams
Security¶
Authentication & Authorization¶
gRPC supports multiple authentication mechanisms:
// JWT Authentication
services.AddGrpc(options =>
{
options.Interceptors.Add<GrpcJwtAuthenticationInterceptor>();
});
TLS/SSL¶
Enable TLS for secure communication:
// Kestrel configuration
"Kestrel": {
"Endpoints": {
"Grpc": {
"Url": "https://localhost:5001",
"Certificate": {
"Path": "/https/aspnetapp.pfx",
"Password": "Password123!"
}
}
}
}
Certificate Validation¶
For client-side certificate validation:
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
Credentials = ChannelCredentials.SecureSsl,
HttpClient = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = ValidateCertificate
})
});
Best Practices¶
Do's¶
- Use Protocol Buffers for All Messages
- Strongly typed contracts
- Efficient serialization
-
Language-agnostic
-
Implement Proper Error Handling
-
Use Streaming for Large Data Sets
-
Validate Requests Early
-
Use Cancellation Tokens
-
Log gRPC Calls
Don'ts¶
-
Don't Return Large Messages
-
Don't Ignore Cancellation
-
Don't Leak Internal Errors
-
Don't Use Synchronous Calls
-
Don't Mix gRPC and REST in Same Endpoint
- Keep protocols separate
- Use different ports/endpoints
- Clear service boundaries
Testing¶
Unit Testing Services¶
[TestMethod]
public async Task GetMicroserviceAggregateRoot_Should_Return_Entity()
{
// Arrange
var retriever = new Mock<IMicroserviceAggregateRootsRetriever>();
var aggregate = new MicroserviceAggregateRootEntity
{
ObjectId = Guid.NewGuid(),
SomeValue = "Test"
};
retriever.Setup(r => r.GetMicroserviceAggregateRootDetails(
It.IsAny<GetMicroserviceAggregateRootDetailsInput>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(aggregate);
var service = new MicroserviceAggregateRootsService(
Mock.Of<ILogger<MicroserviceAggregateRootsService>>(),
retriever.Object,
Mock.Of<IMicroserviceAggregateRootsProcessor>());
var request = new GetMicroserviceAggregateRootRequest
{
ObjectId = aggregate.ObjectId.ToString()
};
// Act
var response = await service.GetMicroserviceAggregateRoot(
request,
Mock.Of<ServerCallContext>());
// Assert
Assert.AreEqual(aggregate.ObjectId.ToString(), response.ObjectId);
}
Integration Testing¶
[TestMethod]
public async Task GetMicroserviceAggregateRoot_Integration_Test()
{
// Arrange
var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
// Create gRPC client
var channel = GrpcChannel.ForAddress(
"http://localhost",
new GrpcChannelOptions { HttpClient = client });
var grpcClient = new MicroserviceAggregateRootsService.MicroserviceAggregateRootsServiceClient(channel);
// Act
var request = new GetMicroserviceAggregateRootRequest
{
ObjectId = Guid.NewGuid().ToString()
};
var response = await grpcClient.GetMicroserviceAggregateRootAsync(request);
// Assert
Assert.IsNotNull(response);
}
Troubleshooting¶
Issue: Connection Refused¶
Symptom: Status(StatusCode="Unavailable", Detail="Error connecting to subchannel")
Causes and Solutions:
- Port Not Configured: Ensure gRPC endpoint is configured in Kestrel
- TLS Mismatch: Verify client and server use compatible TLS settings
- Firewall: Check firewall rules allow gRPC traffic
Issue: Message Too Large¶
Symptom: Status(StatusCode="ResourceExhausted", Detail="Received message larger than max")
Solution: Increase message size limits:
services.AddGrpc(options =>
{
options.MaxReceiveMessageSize = 10 * 1024 * 1024; // 10 MB
options.MaxSendMessageSize = 10 * 1024 * 1024;
});
Issue: Deadline Exceeded¶
Symptom: Status(StatusCode="DeadlineExceeded")
Causes and Solutions:
- Long-Running Operations: Use streaming or background jobs
- Network Issues: Check network connectivity and latency
- Server Overload: Monitor server resources and scale if needed
Issue: Protocol Negotiation Failed¶
Symptom: HTTP/2 protocol errors
Causes and Solutions:
- Reverse Proxy: Ensure proxy supports HTTP/2 (Nginx 1.13.9+, IIS 10+)
- ALPN Support: Verify Application-Layer Protocol Negotiation is enabled
- TLS Configuration: Check TLS/SSL certificate configuration
Summary¶
gRPC in the ConnectSoft Microservice Template provides:
- ✅ High Performance: HTTP/2 and Protocol Buffers for efficient communication
- ✅ Strongly Typed: Code generation from
.protodefinitions - ✅ Streaming Support: Unary, server, client, and bidirectional streaming
- ✅ Error Handling: Rich error details with standard status codes
- ✅ Health Checks: Standard gRPC health protocol support
- ✅ Observability: Structured logging and distributed tracing
- ✅ Security: TLS/SSL, authentication, and authorization support
- ✅ Testing: Unit and integration testing utilities
By following these patterns, microservices achieve:
- Performance — Lower latency and higher throughput than REST
- Type Safety — Compile-time checking of service contracts
- Efficiency — Binary serialization and HTTP/2 multiplexing
- Scalability — Streaming for large datasets and real-time communication
- Reliability — Standard error handling and health checks
- Observability — Comprehensive logging and tracing integration
The gRPC integration ensures that ConnectSoft microservices can efficiently communicate with each other while maintaining type safety, performance, and excellent observability.