# gRPC Validation gRPC validation with Google Rich Error Model for structured error responses. ## Overview When validation fails for gRPC endpoints, the framework returns structured errors following the **Google Rich Error Model**. This provides: - ✅ **Standardized format** - Consistent with Google APIs - ✅ **Field violations** - Know exactly which fields failed - ✅ **Status codes** - gRPC status codes (INVALID_ARGUMENT) - ✅ **Error details** - Additional context via google.rpc types - ✅ **Language-agnostic** - Works across all gRPC implementations ## Google Rich Error Model ### Error Structure ```protobuf google.rpc.Status { code: 3 // INVALID_ARGUMENT message: "Validation failed" details: [ google.rpc.BadRequest { field_violations: [ { field: "name", description: "Name is required" }, { field: "email", description: "Valid email address is required" }, { field: "age", description: "User must be at least 18 years old" } ] } ] } ``` ### gRPC Status Codes - **INVALID_ARGUMENT (3)** - Validation errors - **UNAUTHENTICATED (16)** - Missing or invalid authentication - **PERMISSION_DENIED (7)** - Authorization failures - **NOT_FOUND (5)** - Entity not found - **ALREADY_EXISTS (6)** - Duplicate entity ## Setup ### Install Required Packages ```bash dotnet add package Grpc.StatusProto ``` ### Proto File Configuration Your `.proto` files must import the error model definitions: ```protobuf syntax = "proto3"; import "google/rpc/status.proto"; import "google/rpc/error_details.proto"; package yourapp; service CommandService { rpc CreateUser (CreateUserCommand) returns (CreateUserResponse); } message CreateUserCommand { string name = 1; string email = 2; int32 age = 3; } message CreateUserResponse { int32 user_id = 1; } ``` ### Service Registration ```csharp var builder = WebApplication.CreateBuilder(args); // Register CQRS services builder.Services.AddSvrntyCQRS(); builder.Services.AddDefaultCommandDiscovery(); // Register commands with validators builder.Services.AddCommand(); builder.Services.AddTransient, CreateUserCommandValidator>(); // Add gRPC builder.Services.AddGrpc(); var app = builder.Build(); // Map auto-generated service implementation app.MapGrpcService(); app.MapGrpcReflectionService(); app.Run(); ``` ## Validation Flow ``` gRPC Request: CreateUser │ ▼ ┌──────────────────┐ │ Deserialize │ │ Proto Message │ └──────┬───────────┘ │ ▼ ┌──────────────────┐ │ Run Validator │ └──────┬───────────┘ │ ├─ Valid ──────────▶ Execute Handler ──▶ Success Response │ └─ Invalid ────────▶ Return RpcException with BadRequest ──▶ INVALID_ARGUMENT ``` ## Example Validation Errors ### Single Field Error **gRPC Request:** ```csharp var client = new CommandService.CommandServiceClient(channel); var request = new CreateUserCommand { Name = "", // Invalid Email = "john@example.com" }; try { var response = await client.CreateUserAsync(request); } catch (RpcException ex) { // ex.StatusCode = StatusCode.InvalidArgument // ex.Message = "Validation failed" var status = ex.GetRpcStatus(); var badRequest = status.GetDetail(); foreach (var violation in badRequest.FieldViolations) { Console.WriteLine($"{violation.Field}: {violation.Description}"); } } ``` **Error Details:** ```protobuf google.rpc.Status { code: 3 message: "Validation failed" details: [ google.rpc.BadRequest { field_violations: [ { field: "name", description: "Name is required" } ] } ] } ``` ### Multiple Field Errors **gRPC Request:** ```csharp var request = new CreateUserCommand { Name = "", // Invalid Email = "invalid", // Invalid Age = 16 // Invalid }; try { var response = await client.CreateUserAsync(request); } catch (RpcException ex) { var status = ex.GetRpcStatus(); var badRequest = status.GetDetail(); // badRequest.FieldViolations contains 3 violations } ``` **Error Details:** ```protobuf google.rpc.Status { code: 3 message: "Validation failed" details: [ google.rpc.BadRequest { field_violations: [ { field: "name", description: "Name is required" }, { field: "email", description: "Valid email address is required" }, { field: "age", description: "User must be at least 18 years old" } ] } ] } ``` ## Client-Side Handling ### C# gRPC Client ```csharp using Grpc.Core; using Google.Rpc; public class UserGrpcClient { private readonly CommandService.CommandServiceClient _client; public async Task CreateUserAsync(string name, string email, int age) { var request = new CreateUserCommand { Name = name, Email = email, Age = age }; try { var response = await _client.CreateUserAsync(request); return response.UserId; } catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument) { var status = ex.GetRpcStatus(); var badRequest = status.GetDetail(); if (badRequest != null) { foreach (var violation in badRequest.FieldViolations) { Console.WriteLine($"Validation error - {violation.Field}: {violation.Description}"); } } return null; } } } ``` ### TypeScript gRPC-Web Client ```typescript import { RpcError } from 'grpc-web'; import { Status } from 'google-rpc/status_pb'; import { BadRequest } from 'google-rpc/error_details_pb'; async function createUser(name: string, email: string, age: number) { const request = new CreateUserCommand(); request.setName(name); request.setEmail(email); request.setAge(age); try { const response = await client.createUser(request, {}); return response.getUserId(); } catch (error) { const rpcError = error as RpcError; if (rpcError.code === 3) { // INVALID_ARGUMENT const status = Status.deserializeBinary(rpcError.metadata['grpc-status-details-bin']); status.getDetailsList().forEach(detail => { if (detail.getTypeUrl().includes('BadRequest')) { const badRequest = BadRequest.deserializeBinary(detail.getValue_asU8()); badRequest.getFieldViolationsList().forEach(violation => { console.error(`${violation.getField()}: ${violation.getDescription()}`); }); } }); } return null; } } ``` ### Go gRPC Client ```go import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/genproto/googleapis/rpc/errdetails" ) func createUser(client pb.CommandServiceClient, name, email string, age int32) (int32, error) { req := &pb.CreateUserCommand{ Name: name, Email: email, Age: age, } resp, err := client.CreateUser(context.Background(), req) if err != nil { if st, ok := status.FromError(err); ok { if st.Code() == codes.InvalidArgument { for _, detail := range st.Details() { if br, ok := detail.(*errdetails.BadRequest); ok { for _, violation := range br.GetFieldViolations() { fmt.Printf("%s: %s\n", violation.Field, violation.Description) } } } } } return 0, err } return resp.UserId, nil } ``` ## Validation Rules ### FluentValidation Integration The framework automatically converts FluentValidation errors to Google Rich Error Model: ```csharp public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required") .MaximumLength(100) .WithMessage("Name must not exceed 100 characters"); RuleFor(x => x.Email) .EmailAddress() .WithMessage("Valid email address is required"); RuleFor(x => x.Age) .GreaterThanOrEqualTo(18) .WithMessage("User must be at least 18 years old"); } } ``` ### Field Name Mapping Field names in error responses match proto field names (case-insensitive): ```protobuf message CreateUserCommand { string name = 1; // Error field: "name" string email = 2; // Error field: "email" int32 age = 3; // Error field: "age" } ``` C# property names are automatically converted to proto field names: ```csharp public class CreateUserCommand { public string Name { get; set; } // Maps to "name" public string Email { get; set; } // Maps to "email" public int Age { get; set; } // Maps to "age" } ``` ## Testing Validation ### Integration Tests ```csharp public class CreateUserCommandGrpcTests : IClassFixture { private readonly CommandService.CommandServiceClient _client; public CreateUserCommandGrpcTests(GrpcTestFixture fixture) { _client = fixture.CreateClient(); } [Fact] public async Task CreateUser_WithMissingName_ReturnsInvalidArgument() { var request = new CreateUserCommand { Name = "", Email = "john@example.com" }; var ex = await Assert.ThrowsAsync( () => _client.CreateUserAsync(request).ResponseAsync); Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode); var status = ex.GetRpcStatus(); var badRequest = status.GetDetail(); Assert.NotNull(badRequest); Assert.Contains(badRequest.FieldViolations, v => v.Field == "name"); } [Fact] public async Task CreateUser_WithValidData_ReturnsUserId() { var request = new CreateUserCommand { Name = "John Doe", Email = "john@example.com", Age = 25 }; var response = await _client.CreateUserAsync(request); Assert.True(response.UserId > 0); } } ``` ## Custom Error Details ### Add Additional Context ```csharp public static class ValidationErrorHelper { public static RpcException CreateValidationException(ValidationResult validationResult) { var badRequest = new BadRequest(); foreach (var error in validationResult.Errors) { badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation { Field = ToCamelCase(error.PropertyName), Description = error.ErrorMessage }); } var status = new Google.Rpc.Status { Code = (int)Code.InvalidArgument, Message = "Validation failed", Details = { Any.Pack(badRequest) } }; return status.ToRpcException(); } private static string ToCamelCase(string value) { if (string.IsNullOrEmpty(value) || char.IsLower(value[0])) return value; return char.ToLower(value[0]) + value.Substring(1); } } ``` ## Best Practices ### ✅ DO - Use Google Rich Error Model for structured errors - Return INVALID_ARGUMENT for validation errors - Provide field-level error messages - Use descriptive error messages - Test validation scenarios - Handle validation errors gracefully in clients - Map C# property names to proto field names ### ❌ DON'T - Don't return INTERNAL for validation errors - Don't use generic error messages - Don't expose internal details - Don't skip validation - Don't throw unhandled exceptions - Don't return plain text error messages ## Comparison: HTTP vs gRPC | Feature | HTTP (RFC 7807) | gRPC (Rich Error Model) | |---------|-----------------|-------------------------| | Format | JSON | Protobuf | | Status | HTTP 400 | INVALID_ARGUMENT (3) | | Field Errors | `errors` object | `field_violations` array | | Structure | `ProblemDetails` | `google.rpc.Status` | | Details | Extensions | Typed details (BadRequest, etc.) | | Size | Larger (JSON) | Smaller (binary) | ## See Also - [Validation Overview](README.md) - [FluentValidation Setup](fluentvalidation-setup.md) - [HTTP Validation](http-validation.md) - [Google Rich Error Model](https://cloud.google.com/apis/design/errors) - [gRPC Status Codes](https://grpc.io/docs/guides/status-codes/)