# Service Implementation Understanding auto-generated gRPC service implementations. ## Generated Services The source generator creates two main service implementations: 1. **CommandServiceImpl** - Handles all command RPCs 2. **QueryServiceImpl** - Handles all query RPCs Both inherit from gRPC-generated base classes and integrate with CQRS handlers. ## CommandServiceImpl ### Structure ```csharp public partial class CommandServiceImpl : CommandService.CommandServiceBase { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public CommandServiceImpl( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } // RPC implementations generated here } ``` ### Command With Result ```csharp public override async Task CreateUser( CreateUserCommand request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); try { // 1. Validate await ValidateAsync(request, scope, context.CancellationToken); // 2. Get handler var handler = scope.ServiceProvider .GetRequiredService>(); // 3. Execute var userId = await handler.HandleAsync(request, context.CancellationToken); // 4. Return response return new CreateUserResponse { UserId = userId }; } catch (KeyNotFoundException ex) { throw new RpcException(new Status(StatusCode.NotFound, ex.Message)); } catch (Exception ex) { _logger.LogError(ex, "Error executing CreateUser"); throw new RpcException(new Status(StatusCode.Internal, "An error occurred")); } } ``` ### Command Without Result ```csharp public override async Task DeleteUser( DeleteUserCommand request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); try { var handler = scope.ServiceProvider .GetRequiredService>(); await handler.HandleAsync(request, context.CancellationToken); return new Empty(); } catch (KeyNotFoundException ex) { throw new RpcException(new Status(StatusCode.NotFound, ex.Message)); } } ``` ## QueryServiceImpl ### Structure ```csharp public partial class QueryServiceImpl : QueryService.QueryServiceBase { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public QueryServiceImpl( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } // RPC implementations generated here } ``` ### Query Implementation ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); try { var handler = scope.ServiceProvider .GetRequiredService>(); var result = await handler.HandleAsync(request, context.CancellationToken); return result; } catch (KeyNotFoundException ex) { throw new RpcException(new Status(StatusCode.NotFound, ex.Message)); } } ``` ## Validation Integration ### Automatic Validation ```csharp private async Task ValidateAsync( TCommand command, IServiceScope scope, CancellationToken cancellationToken) { var validator = scope.ServiceProvider.GetService>(); if (validator == null) return; var validationResult = await validator.ValidateAsync(command, cancellationToken); if (!validationResult.IsValid) { throw CreateValidationException(validationResult); } } ``` ### Validation Exception ```csharp private 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 string ToCamelCase(string value) { if (string.IsNullOrEmpty(value) || char.IsLower(value[0])) return value; return char.ToLower(value[0]) + value.Substring(1); } ``` ## Error Handling ### Exception to StatusCode Mapping ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { try { // Handler execution } catch (KeyNotFoundException ex) { throw new RpcException(new Status(StatusCode.NotFound, ex.Message)); } catch (UnauthorizedAccessException ex) { throw new RpcException(new Status(StatusCode.PermissionDenied, ex.Message)); } catch (ArgumentException ex) { throw new RpcException(new Status(StatusCode.InvalidArgument, ex.Message)); } catch (Exception ex) { _logger.LogError(ex, "Unhandled error in GetUser"); throw new RpcException(new Status(StatusCode.Internal, "An error occurred")); } } ``` ### Status Code Reference | Exception | gRPC Status Code | Description | |-----------|------------------|-------------| | `KeyNotFoundException` | NOT_FOUND (5) | Entity not found | | `UnauthorizedAccessException` | PERMISSION_DENIED (7) | Authorization failure | | `ArgumentException` | INVALID_ARGUMENT (3) | Invalid input | | `ValidationException` | INVALID_ARGUMENT (3) | Validation failure | | `TimeoutException` | DEADLINE_EXCEEDED (4) | Operation timeout | | Generic `Exception` | INTERNAL (13) | Unknown error | ## Dependency Injection ### Scoped Services ```csharp public override async Task CreateUser( CreateUserCommand request, ServerCallContext context) { // Create scope for request using var scope = _serviceProvider.CreateScope(); // Resolve scoped services var handler = scope.ServiceProvider .GetRequiredService>(); var validator = scope.ServiceProvider .GetService>(); // Execute... } ``` ### Why Scoping? - **DbContext per request** - Entity Framework requires scoped DbContext - **Clean disposal** - Resources disposed after request - **Isolation** - Each request gets its own service instances ## Logging ### Request Logging ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { _logger.LogInformation( "GetUser request: UserId={UserId}, Client={Peer}", request.UserId, context.Peer); try { var result = await ExecuteQuery(request, context); _logger.LogInformation("GetUser completed successfully: UserId={UserId}", request.UserId); return result; } catch (Exception ex) { _logger.LogError(ex, "GetUser failed: UserId={UserId}", request.UserId); throw; } } ``` ### Performance Logging ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { var stopwatch = Stopwatch.StartNew(); try { var result = await ExecuteQuery(request, context); stopwatch.Stop(); _logger.LogInformation( "GetUser completed in {ElapsedMs}ms: UserId={UserId}", stopwatch.ElapsedMilliseconds, request.UserId); return result; } catch { stopwatch.Stop(); _logger.LogWarning( "GetUser failed after {ElapsedMs}ms: UserId={UserId}", stopwatch.ElapsedMilliseconds, request.UserId); throw; } } ``` ## Metadata & Headers ### Reading Metadata ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { // Read request headers var metadata = context.RequestHeaders; var correlationId = metadata.GetValue("correlation-id"); var clientVersion = metadata.GetValue("client-version"); _logger.LogInformation( "GetUser: CorrelationId={CorrelationId}, ClientVersion={ClientVersion}", correlationId, clientVersion); // Execute... } ``` ### Writing Response Headers ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { // Add response headers await context.WriteResponseHeadersAsync(new Metadata { { "server-version", "1.0.0" }, { "request-id", Guid.NewGuid().ToString() } }); // Execute query... } ``` ## Deadlines & Cancellation ### Respecting Deadlines ```csharp public override async Task GetUser( GetUserQuery request, ServerCallContext context) { // Check if deadline exceeded if (context.CancellationToken.IsCancellationRequested) { throw new RpcException(new Status(StatusCode.DeadlineExceeded, "Request deadline exceeded")); } // Pass cancellation token to handler var handler = GetHandler>(scope); var result = await handler.HandleAsync(request, context.CancellationToken); return result; } ``` ## Interceptors ### Custom Interceptors While service implementations are auto-generated, you can add interceptors: ```csharp public class LoggingInterceptor : Interceptor { private readonly ILogger _logger; public LoggingInterceptor(ILogger logger) { _logger = logger; } public override async Task UnaryServerHandler( TRequest request, ServerCallContext context, UnaryServerMethod continuation) { _logger.LogInformation("gRPC call: {Method}", context.Method); try { return await continuation(request, context); } catch (Exception ex) { _logger.LogError(ex, "gRPC error: {Method}", context.Method); throw; } } } // Registration builder.Services.AddGrpc(options => { options.Interceptors.Add(); }); ``` ## Testing ### Unit Testing ```csharp public class CommandServiceImplTests { private readonly Mock _mockServiceProvider; private readonly Mock _mockScope; private readonly CommandServiceImpl _service; public CommandServiceImplTests() { _mockServiceProvider = new Mock(); _mockScope = new Mock(); _mockServiceProvider .Setup(sp => sp.CreateScope()) .Returns(_mockScope.Object); _service = new CommandServiceImpl(_mockServiceProvider.Object, Mock.Of>()); } [Fact] public async Task CreateUser_WithValidData_ReturnsUserId() { // Arrange var mockHandler = new Mock>(); mockHandler .Setup(h => h.HandleAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(42); _mockScope .Setup(s => s.ServiceProvider.GetService(typeof(ICommandHandler))) .Returns(mockHandler.Object); var request = new CreateUserCommand { Name = "John", Email = "john@example.com" }; var context = TestServerCallContext.Create(); // Act var response = await _service.CreateUser(request, context); // Assert Assert.Equal(42, response.UserId); } } ``` ## Best Practices ### ✅ DO - Use scoped services for each request - Log important operations - Handle exceptions appropriately - Pass cancellation tokens - Use dependency injection - Respect deadlines - Add metadata for tracing ### ❌ DON'T - Don't catch and swallow exceptions - Don't ignore cancellation tokens - Don't use static dependencies - Don't skip validation - Don't leak implementation details in errors - Don't block async operations ## See Also - [gRPC Integration Overview](README.md) - [Getting Started](getting-started-grpc.md) - [Source Generators](source-generators.md) - [gRPC Clients](grpc-clients.md) - [gRPC Troubleshooting](grpc-troubleshooting.md)