523 lines
12 KiB
Markdown
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)
|