Skip to content

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 .proto definitions
  • 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

  1. Field Numbers: Use field numbers 1-15 for frequently used fields (1 byte encoding)
  2. Naming Convention: Use snake_case for field names, PascalCase for message types
  3. Package Names: Use reverse domain notation: connectsoft.microservicetemplate
  4. Versioning: Use package versions for breaking changes: connectsoft.microservicetemplate.v2
  5. Deprecation: Use deprecated = true for 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-Encoding and Accept-Encoding headers, configured via CompressionOptions
  • gRPC Compression: Uses grpc-encoding and grpc-accept-encoding headers, configured via GrpcOptions

Both can be enabled simultaneously for different protocols.

Compression Providers

The template supports the following compression providers:

  • Gzip (GzipCompressionProvider) - Built-in provider from Grpc.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 in Grpc.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 ratio
  • Optimal - Balanced compression (recommended default)
  • SmallestSize - Maximum compression, higher CPU usage
  • NoCompression - No compression

How It Works

  1. Client Request: The client sends a grpc-accept-encoding header indicating supported compression algorithms (e.g., grpc-accept-encoding: gzip, br)

  2. Server Response: If compression is enabled and the client supports it, the server:

  3. Compresses the response using the configured algorithm
  4. Sets the grpc-encoding header to indicate the compression used
  5. Sends the compressed response

  6. Client Decompression: The client automatically decompresses the response based on the grpc-encoding header

Best Practices

  • When to Enable: Enable compression for gRPC services that send large payloads or when bandwidth is a concern
  • Algorithm Selection:
  • Use gzip for maximum compatibility
  • Use br (Brotli) for better compression ratios when client support is available
  • Compression Level:
  • Use Optimal for most scenarios (balanced performance and compression)
  • Use Fastest for high-traffic scenarios
  • Use SmallestSize for 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

{
  "Grpc": {
    "ResponseCompressionAlgorithm": "br",
    "ResponseCompressionLevel": "Optimal"
  }
}

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

endpoints.MapGrpcService<HealthService>();

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

  1. Use Protocol Buffers for All Messages
  2. Strongly typed contracts
  3. Efficient serialization
  4. Language-agnostic

  5. Implement Proper Error Handling

    // ✅ GOOD - Use appropriate status codes
    throw new RpcException(
        new Status(StatusCode.NotFound, "Resource not found"));
    

  6. Use Streaming for Large Data Sets

    // ✅ GOOD - Stream large result sets
    await foreach (var item in GetLargeDataSet())
    {
        await responseStream.WriteAsync(item);
    }
    

  7. Validate Requests Early

    // ✅ GOOD - Validate before processing
    if (string.IsNullOrEmpty(request.ObjectId))
    {
        throw new RpcException(
            new Status(StatusCode.InvalidArgument, "ObjectId required"));
    }
    

  8. Use Cancellation Tokens

    // ✅ GOOD - Respect cancellation
    await operation(context.CancellationToken);
    

  9. Log gRPC Calls

    // ✅ GOOD - Structured logging
    this.logger.LogInformation(
        "gRPC call {Method} from {Peer}",
        context.Method, 
        context.Peer);
    

Don'ts

  1. Don't Return Large Messages

    // ❌ BAD - Large response in unary call
    return new LargeResponse { Data = hugeByteArray };
    
    // ✅ GOOD - Use streaming
    await foreach (var chunk in GetLargeDataStream())
    {
        await responseStream.WriteAsync(chunk);
    }
    

  2. Don't Ignore Cancellation

    // ❌ BAD - Ignoring cancellation
    await LongRunningOperation();
    
    // ✅ GOOD - Respect cancellation
    await LongRunningOperation(context.CancellationToken);
    

  3. Don't Leak Internal Errors

    // ❌ BAD - Exposing internal details
    throw new RpcException(
        new Status(StatusCode.Internal, ex.StackTrace));
    
    // ✅ GOOD - Generic error message
    throw new RpcException(
        new Status(StatusCode.Internal, "An error occurred"));
    

  4. Don't Use Synchronous Calls

    // ❌ BAD - Blocking call
    var response = client.GetData(request).ResponseAsync.Result;
    
    // ✅ GOOD - Async call
    var response = await client.GetDataAsync(request);
    

  5. Don't Mix gRPC and REST in Same Endpoint

  6. Keep protocols separate
  7. Use different ports/endpoints
  8. 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:

  1. Port Not Configured: Ensure gRPC endpoint is configured in Kestrel
  2. TLS Mismatch: Verify client and server use compatible TLS settings
  3. 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:

  1. Long-Running Operations: Use streaming or background jobs
  2. Network Issues: Check network connectivity and latency
  3. Server Overload: Monitor server resources and scale if needed

Issue: Protocol Negotiation Failed

Symptom: HTTP/2 protocol errors

Causes and Solutions:

  1. Reverse Proxy: Ensure proxy supports HTTP/2 (Nginx 1.13.9+, IIS 10+)
  2. ALPN Support: Verify Application-Layer Protocol Negotiation is enabled
  3. 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 .proto definitions
  • 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.