12 KiB
12 KiB
Service Implementation
Understanding auto-generated gRPC service implementations.
Generated Services
The source generator creates two main service implementations:
- CommandServiceImpl - Handles all command RPCs
- QueryServiceImpl - Handles all query RPCs
Both inherit from gRPC-generated base classes and integrate with CQRS handlers.
CommandServiceImpl
Structure
public partial class CommandServiceImpl : CommandService.CommandServiceBase
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CommandServiceImpl> _logger;
public CommandServiceImpl(
IServiceProvider serviceProvider,
ILogger<CommandServiceImpl> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
// RPC implementations generated here
}
Command With Result
public override async Task<CreateUserResponse> 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<ICommandHandler<CreateUserCommand, int>>();
// 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
public override async Task<Empty> DeleteUser(
DeleteUserCommand request,
ServerCallContext context)
{
using var scope = _serviceProvider.CreateScope();
try
{
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<DeleteUserCommand>>();
await handler.HandleAsync(request, context.CancellationToken);
return new Empty();
}
catch (KeyNotFoundException ex)
{
throw new RpcException(new Status(StatusCode.NotFound, ex.Message));
}
}
QueryServiceImpl
Structure
public partial class QueryServiceImpl : QueryService.QueryServiceBase
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<QueryServiceImpl> _logger;
public QueryServiceImpl(
IServiceProvider serviceProvider,
ILogger<QueryServiceImpl> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
// RPC implementations generated here
}
Query Implementation
public override async Task<UserDto> GetUser(
GetUserQuery request,
ServerCallContext context)
{
using var scope = _serviceProvider.CreateScope();
try
{
var handler = scope.ServiceProvider
.GetRequiredService<IQueryHandler<GetUserQuery, UserDto>>();
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
private async Task ValidateAsync<TCommand>(
TCommand command,
IServiceScope scope,
CancellationToken cancellationToken)
{
var validator = scope.ServiceProvider.GetService<IValidator<TCommand>>();
if (validator == null)
return;
var validationResult = await validator.ValidateAsync(command, cancellationToken);
if (!validationResult.IsValid)
{
throw CreateValidationException(validationResult);
}
}
Validation Exception
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
public override async Task<UserDto> 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
public override async Task<CreateUserResponse> CreateUser(
CreateUserCommand request,
ServerCallContext context)
{
// Create scope for request
using var scope = _serviceProvider.CreateScope();
// Resolve scoped services
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<CreateUserCommand, int>>();
var validator = scope.ServiceProvider
.GetService<IValidator<CreateUserCommand>>();
// 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
public override async Task<UserDto> 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
public override async Task<UserDto> 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
public override async Task<UserDto> 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
public override async Task<UserDto> 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
public override async Task<UserDto> 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<IQueryHandler<GetUserQuery, UserDto>>(scope);
var result = await handler.HandleAsync(request, context.CancellationToken);
return result;
}
Interceptors
Custom Interceptors
While service implementations are auto-generated, you can add interceptors:
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)
{
_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<LoggingInterceptor>();
});
Testing
Unit Testing
public class CommandServiceImplTests
{
private readonly Mock<IServiceProvider> _mockServiceProvider;
private readonly Mock<IServiceScope> _mockScope;
private readonly CommandServiceImpl _service;
public CommandServiceImplTests()
{
_mockServiceProvider = new Mock<IServiceProvider>();
_mockScope = new Mock<IServiceScope>();
_mockServiceProvider
.Setup(sp => sp.CreateScope())
.Returns(_mockScope.Object);
_service = new CommandServiceImpl(_mockServiceProvider.Object, Mock.Of<ILogger<CommandServiceImpl>>());
}
[Fact]
public async Task CreateUser_WithValidData_ReturnsUserId()
{
// Arrange
var mockHandler = new Mock<ICommandHandler<CreateUserCommand, int>>();
mockHandler
.Setup(h => h.HandleAsync(It.IsAny<CreateUserCommand>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(42);
_mockScope
.Setup(s => s.ServiceProvider.GetService(typeof(ICommandHandler<CreateUserCommand, int>)))
.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