# 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 CreateUser( CreateUserCommand request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); // Get validator var validator = scope.ServiceProvider .GetService>(); // 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>(); // Execute var userId = await handler.HandleAsync(request, context.CancellationToken); // Return response return new CreateUserResponse { UserId = userId }; } public override async Task DeleteUser( DeleteUserCommand request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); var handler = scope.ServiceProvider .GetRequiredService>(); 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 GetUser( GetUserQuery request, ServerCallContext context) { using var scope = _serviceProvider.CreateScope(); var handler = scope.ServiceProvider .GetRequiredService>(); 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` | | `DeleteUserCommand` | `DeleteUserCommand` | `ICommandHandler` | | `GetUserQuery` | `GetUserQuery` | `IQueryHandler` | | `UserDto` | `UserDto` | Return type | ### Response Type Detection **Command with result:** ```protobuf rpc CreateUser (CreateUserCommand) returns (CreateUserResponse); ``` Generator looks for: `ICommandHandler` **Command without result:** ```protobuf rpc DeleteUser (DeleteUserCommand) returns (google.protobuf.Empty); ``` Generator looks for: `ICommandHandler` ## Build Integration ### Project Configuration **.csproj:** ```xml net10.0 all runtime; build; native; contentfiles; analyzers ``` ### 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 all analyzers ``` ### 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 ``` **Cause:** Handler not registered in DI **Solution:** ```csharp builder.Services.AddCommand(); ``` #### 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 ``` 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 ``` ### Shared Proto Files ```xml ``` ## 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)