dotnet-cqrs/docs/grpc-integration/source-generators.md

467 lines
11 KiB
Markdown

# Source Generators
How automatic gRPC service implementation generation works.
## Overview
`Svrnty.CQRS.Grpc.Generators` uses Roslyn source generators to automatically create gRPC service implementations at compile time. This eliminates boilerplate code and ensures type safety between .proto definitions and C# handlers.
**Benefits:**
-**Zero boilerplate** - No manual service implementation
-**Compile-time safety** - Errors caught during build
-**Type checking** - Ensures proto and C# types match
-**Automatic updates** - Regenerates when proto changes
-**IDE support** - IntelliSense for generated code
## How It Works
```
┌──────────────────────────────────┐
│ Build Process │
├──────────────────────────────────┤
│ 1. Compile .proto files │
│ 2. Generate C# types (Grpc.Tools)│
│ 3. Source generator runs │
│ - Reads proto definitions │
│ - Discovers CQRS handlers │
│ - Generates service impls │
│ 4. Compile generated code │
│ 5. Build completes │
└──────────────────────────────────┘
```
## Generated Code
### CommandServiceImpl
**From .proto:**
```protobuf
service CommandService {
rpc CreateUser (CreateUserCommand) returns (CreateUserResponse);
rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
}
```
**Generated C# (simplified):**
```csharp
public class CommandServiceImpl : CommandService.CommandServiceBase
{
private readonly IServiceProvider _serviceProvider;
public CommandServiceImpl(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override async Task<CreateUserResponse> CreateUser(
CreateUserCommand request,
ServerCallContext context)
{
using var scope = _serviceProvider.CreateScope();
// Get validator
var validator = scope.ServiceProvider
.GetService<IValidator<CreateUserCommand>>();
// Validate
if (validator != null)
{
var validationResult = await validator.ValidateAsync(
request,
context.CancellationToken);
if (!validationResult.IsValid)
{
throw CreateValidationException(validationResult);
}
}
// Get handler
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<CreateUserCommand, int>>();
// Execute
var userId = await handler.HandleAsync(request, context.CancellationToken);
// Return response
return new CreateUserResponse { UserId = userId };
}
public override async Task<Empty> DeleteUser(
DeleteUserCommand request,
ServerCallContext context)
{
using var scope = _serviceProvider.CreateScope();
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<DeleteUserCommand>>();
await handler.HandleAsync(request, context.CancellationToken);
return new Empty();
}
private RpcException CreateValidationException(ValidationResult validationResult)
{
var badRequest = new BadRequest();
foreach (var error in validationResult.Errors)
{
badRequest.FieldViolations.Add(new 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();
}
}
```
### QueryServiceImpl
**From .proto:**
```protobuf
service QueryService {
rpc GetUser (GetUserQuery) returns (UserDto);
}
```
**Generated C# (simplified):**
```csharp
public class QueryServiceImpl : QueryService.QueryServiceBase
{
private readonly IServiceProvider _serviceProvider;
public QueryServiceImpl(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override async Task<UserDto> GetUser(
GetUserQuery request,
ServerCallContext context)
{
using var scope = _serviceProvider.CreateScope();
var handler = scope.ServiceProvider
.GetRequiredService<IQueryHandler<GetUserQuery, UserDto>>();
var result = await handler.HandleAsync(request, context.CancellationToken);
return result;
}
}
```
## Type Mapping
### Proto to C# Mapping
The source generator maps .proto types to C# CQRS types:
| Proto Message | C# Type | Handler Type |
|---------------|---------|--------------|
| `CreateUserCommand` | `CreateUserCommand` | `ICommandHandler<CreateUserCommand, TResult>` |
| `DeleteUserCommand` | `DeleteUserCommand` | `ICommandHandler<DeleteUserCommand>` |
| `GetUserQuery` | `GetUserQuery` | `IQueryHandler<GetUserQuery, UserDto>` |
| `UserDto` | `UserDto` | Return type |
### Response Type Detection
**Command with result:**
```protobuf
rpc CreateUser (CreateUserCommand) returns (CreateUserResponse);
```
Generator looks for: `ICommandHandler<CreateUserCommand, TResult>`
**Command without result:**
```protobuf
rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty);
```
Generator looks for: `ICommandHandler<DeleteUserCommand>`
## Build Integration
### Project Configuration
**.csproj:**
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Include .proto files -->
<Protobuf Include="Protos\**\*.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<!-- Source generator package -->
<PackageReference Include="Svrnty.CQRS.Grpc.Generators" Version="1.0.0" />
<!-- gRPC packages -->
<PackageReference Include="Grpc.AspNetCore" Version="2.68.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
```
### Build Output
During build, you'll see:
```
Restoring NuGet packages...
Generating C# from .proto files...
Running source generators...
- Svrnty.CQRS.Grpc.Generators
Generated: CommandServiceImpl.g.cs
Generated: QueryServiceImpl.g.cs
Compiling...
Build succeeded.
```
## Viewing Generated Code
### In Visual Studio
1. Expand project in Solution Explorer
2. Expand "Dependencies" → "Analyzers" → "Svrnty.CQRS.Grpc.Generators"
3. View generated files
### In Rider
1. Navigate to a service usage
2. Right-click → "Go to Declaration"
3. View generated implementation
### Output Directory
Generated files are written to:
```
obj/Debug/net10.0/generated/Svrnty.CQRS.Grpc.Generators/
```
### Manual Inspection
```bash
# View generated files
cat obj/Debug/net10.0/generated/Svrnty.CQRS.Grpc.Generators/*.cs
```
## Customization
### Disabling Generation
Temporarily disable the generator:
**.csproj:**
```xml
<ItemGroup>
<PackageReference Include="Svrnty.CQRS.Grpc.Generators" Version="1.0.0">
<!-- Disable analyzer/generator -->
<PrivateAssets>all</PrivateAssets>
<ExcludeAssets>analyzers</ExcludeAssets>
</PackageReference>
</ItemGroup>
```
### Excluding Specific Services
Use `[GrpcIgnore]` attribute:
```csharp
[GrpcIgnore]
public record InternalCommand
{
public int Id { get; init; }
}
```
No gRPC RPC will be generated for this command.
## Error Handling
### Common Build Errors
#### Error: Handler not found
**Message:**
```
Could not find ICommandHandler<CreateUserCommand, int>
```
**Cause:** Handler not registered in DI
**Solution:**
```csharp
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
```
#### Error: Type mismatch
**Message:**
```
CreateUserCommand in .proto does not match C# type
```
**Cause:** Property names don't match
**Proto:**
```protobuf
message CreateUserCommand {
string user_name = 1; // snake_case
}
```
**C#:**
```csharp
public record CreateUserCommand
{
public string UserName { get; init; } // PascalCase - OK
}
```
Properties match (case-insensitive) - this should work.
**Proto:**
```protobuf
message CreateUserCommand {
string name = 1;
}
```
**C#:**
```csharp
public record CreateUserCommand
{
public string FullName { get; init; } // Different name - ERROR
}
```
**Solution:** Ensure property names match (case-insensitive).
#### Error: Circular dependency
**Message:**
```
Circular reference detected in message definitions
```
**Cause:** Proto messages reference each other
**Solution:** Break circular reference using separate DTOs.
## Performance
### Compile-Time Generation
- **Zero runtime overhead** - Code generated at build time
- **No reflection** - Direct method calls
- **Optimized** - IL identical to hand-written code
### Incremental Build
Source generators support incremental builds:
- Only regenerate when .proto files change
- Fast rebuilds when only C# code changes
## Troubleshooting
### Generator Not Running
**Symptoms:** No service implementations generated
**Checks:**
1. Verify package installed:
```xml
<PackageReference Include="Svrnty.CQRS.Grpc.Generators" Version="1.0.0" />
```
2. Clean and rebuild:
```bash
dotnet clean
dotnet build
```
3. Check build output for generator messages
### Generated Code Not Visible
**Symptoms:** Can't find generated classes in IDE
**Solutions:**
1. Close and reopen solution
2. Rebuild project
3. Restart IDE
4. Check obj/Debug/net10.0/generated/ directory
### Build Warnings
**Warning:** "Generator produced no output"
**Cause:** No matching handlers found
**Solution:** Ensure handlers are registered before build.
## Best Practices
### ✅ DO
- Keep .proto files in Protos/ directory
- Use GrpcServices="Server" in .csproj
- Register all handlers in DI
- Clean build after .proto changes
- Review generated code occasionally
### ❌ DON'T
- Don't modify generated files (they'll be overwritten)
- Don't commit generated files to source control
- Don't disable generators without reason
- Don't ignore build warnings
## Advanced Scenarios
### Multiple Proto Files
```xml
<ItemGroup>
<Protobuf Include="Protos\commands.proto" GrpcServices="Server" />
<Protobuf Include="Protos\queries.proto" GrpcServices="Server" />
<Protobuf Include="Protos\common.proto" GrpcServices="None" />
</ItemGroup>
```
### Shared Proto Files
```xml
<ItemGroup>
<!-- Reference shared proto from another project -->
<Protobuf Include="..\Shared.Contracts\Protos\*.proto"
GrpcServices="Client"
Link="Protos\Shared\%(Filename)%(Extension)" />
</ItemGroup>
```
## See Also
- [gRPC Integration Overview](README.md)
- [Getting Started](getting-started-grpc.md)
- [Proto File Setup](proto-file-setup.md)
- [Service Implementation](service-implementation.md)
- [gRPC Troubleshooting](grpc-troubleshooting.md)
- [Roslyn Source Generators](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)