dotnet-cqrs/docs/grpc-integration/service-implementation.md

523 lines
12 KiB
Markdown

# 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<CommandServiceImpl> _logger;
public CommandServiceImpl(
IServiceProvider serviceProvider,
ILogger<CommandServiceImpl> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
// RPC implementations generated here
}
```
### Command With Result
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```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<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
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```csharp
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:
```csharp
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
```csharp
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
## 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)