Skip to main content
The Intent.AspNetCore.Grpc module generates high-performance gRPC services based on definitions in the Services Designer. gRPC is a modern, high-performance RPC framework ideal for microservices and mobile-to-backend communication.

Overview

gRPC uses Protocol Buffers (protobuf) as its Interface Definition Language (IDL) and provides features like bidirectional streaming, flow control, and efficient binary serialization. This module automatically generates .proto files and service implementations from your Intent Architect service models.

What Gets Generated

Proto Files

Protocol Buffer definitions for your services:
syntax = "proto3";

package myapp.v1;

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

service ProductService {
  rpc GetProduct (GetProductRequest) returns (ProductResponse);
  rpc ListProducts (ListProductsRequest) returns (ListProductsResponse);
  rpc CreateProduct (CreateProductRequest) returns (ProductResponse);
  rpc UpdateProduct (UpdateProductRequest) returns (google.protobuf.Empty);
  rpc DeleteProduct (DeleteProductRequest) returns (google.protobuf.Empty);
}

message ProductResponse {
  string id = 1;
  string name = 2;
  double price = 3;
  google.protobuf.Timestamp created_date = 4;
}

message GetProductRequest {
  string id = 1;
}

message ListProductsRequest {
  int32 page_size = 1;
  int32 page_number = 2;
}

message ListProductsResponse {
  repeated ProductResponse products = 1;
  int32 total_count = 2;
}

message CreateProductRequest {
  string name = 1;
  double price = 2;
}

message UpdateProductRequest {
  string id = 1;
  string name = 2;
  double price = 3;
}

message DeleteProductRequest {
  string id = 1;
}

gRPC Service Implementation

Generated service classes:
public class ProductService : ProductServiceBase
{
    private readonly IMediator _mediator;
    private readonly IMapper _mapper;

    public ProductService(IMediator mediator, IMapper mapper)
    {
        _mediator = mediator;
        _mapper = mapper;
    }

    public override async Task<ProductResponse> GetProduct(
        GetProductRequest request,
        ServerCallContext context)
    {
        var query = new GetProductByIdQuery { Id = Guid.Parse(request.Id) };
        var result = await _mediator.Send(query, context.CancellationToken);
        
        return _mapper.Map<ProductResponse>(result);
    }

    public override async Task<ListProductsResponse> ListProducts(
        ListProductsRequest request,
        ServerCallContext context)
    {
        var query = new GetAllProductsQuery
        {
            PageSize = request.PageSize,
            PageNumber = request.PageNumber
        };
        
        var result = await _mediator.Send(query, context.CancellationToken);
        
        return new ListProductsResponse
        {
            Products = { _mapper.Map<List<ProductResponse>>(result.Items) },
            TotalCount = result.TotalCount
        };
    }

    public override async Task<ProductResponse> CreateProduct(
        CreateProductRequest request,
        ServerCallContext context)
    {
        var command = _mapper.Map<CreateProductCommand>(request);
        var result = await _mediator.Send(command, context.CancellationToken);
        
        return _mapper.Map<ProductResponse>(result);
    }

    public override async Task<Empty> UpdateProduct(
        UpdateProductRequest request,
        ServerCallContext context)
    {
        var command = _mapper.Map<UpdateProductCommand>(request);
        await _mediator.Send(command, context.CancellationToken);
        
        return new Empty();
    }

    public override async Task<Empty> DeleteProduct(
        DeleteProductRequest request,
        ServerCallContext context)
    {
        var command = new DeleteProductCommand { Id = Guid.Parse(request.Id) };
        await _mediator.Send(command, context.CancellationToken);
        
        return new Empty();
    }
}

GrpcConfiguration

Configures gRPC services:
public static class GrpcConfiguration
{
    public static IServiceCollection AddGrpcConfiguration(
        this IServiceCollection services)
    {
        services.AddGrpc(options =>
        {
            options.EnableDetailedErrors = true;
            options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
            options.MaxSendMessageSize = 4 * 1024 * 1024; // 4 MB
        });

        services.AddGrpcReflection();

        return services;
    }

    public static IApplicationBuilder UseGrpcConfiguration(
        this IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<ProductService>();
            endpoints.MapGrpcReflectionService();
        });

        return app;
    }
}

Exception Interceptor

Handles exceptions gracefully:
public class GrpcExceptionInterceptor : Interceptor
{
    private readonly ILogger<GrpcExceptionInterceptor> _logger;

    public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await continuation(request, context);
        }
        catch (NotFoundException ex)
        {
            _logger.LogWarning(ex, "Not found: {Message}", ex.Message);
            throw new RpcException(new Status(StatusCode.NotFound, ex.Message));
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(ex, "Validation failed: {Message}", ex.Message);
            throw new RpcException(new Status(StatusCode.InvalidArgument, ex.Message));
        }
        catch (UnauthorizedAccessException ex)
        {
            _logger.LogWarning(ex, "Unauthorized: {Message}", ex.Message);
            throw new RpcException(new Status(StatusCode.Unauthenticated, ex.Message));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception in gRPC call");
            throw new RpcException(new Status(StatusCode.Internal, "An error occurred"));
        }
    }
}

Key Features

High Performance

Binary protocol with efficient serialization

Streaming

Support for unary, server, client, and bidirectional streaming

Type Safety

Strong typing with auto-generated client code

Multi-Platform

Clients for .NET, Java, Python, Go, and more

Service Types

Unary RPC

Simple request-response:
rpc GetProduct (GetProductRequest) returns (ProductResponse);
public override async Task<ProductResponse> GetProduct(
    GetProductRequest request,
    ServerCallContext context)
{
    // Return single response
}

Server Streaming

Server streams multiple responses:
rpc StreamProducts (StreamProductsRequest) returns (stream ProductResponse);
public override async Task StreamProducts(
    StreamProductsRequest request,
    IServerStreamWriter<ProductResponse> responseStream,
    ServerCallContext context)
{
    var products = await _repository.GetAllAsync();
    
    foreach (var product in products)
    {
        await responseStream.WriteAsync(_mapper.Map<ProductResponse>(product));
        await Task.Delay(100); // Simulate streaming
    }
}

Client Streaming

Client streams multiple requests:
rpc UploadProducts (stream CreateProductRequest) returns (UploadSummary);
public override async Task<UploadSummary> UploadProducts(
    IAsyncStreamReader<CreateProductRequest> requestStream,
    ServerCallContext context)
{
    var count = 0;
    
    await foreach (var request in requestStream.ReadAllAsync())
    {
        var command = _mapper.Map<CreateProductCommand>(request);
        await _mediator.Send(command, context.CancellationToken);
        count++;
    }
    
    return new UploadSummary { ProductsCreated = count };
}

Bidirectional Streaming

Both client and server stream:
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
public override async Task Chat(
    IAsyncStreamReader<ChatMessage> requestStream,
    IServerStreamWriter<ChatMessage> responseStream,
    ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync())
    {
        // Process message and respond
        var response = new ChatMessage
        {
            User = "Server",
            Text = $"Echo: {message.Text}"
        };
        
        await responseStream.WriteAsync(response);
    }
}

Client Usage

.NET Client

public class ProductGrpcClient
{
    private readonly ProductService.ProductServiceClient _client;

    public ProductGrpcClient(string address)
    {
        var channel = GrpcChannel.ForAddress(address);
        _client = new ProductService.ProductServiceClient(channel);
    }

    public async Task<ProductResponse> GetProductAsync(string id)
    {
        var request = new GetProductRequest { Id = id };
        return await _client.GetProductAsync(request);
    }

    public async Task<List<ProductResponse>> GetAllProductsAsync()
    {
        var request = new ListProductsRequest
        {
            PageSize = 100,
            PageNumber = 1
        };
        
        var response = await _client.ListProductsAsync(request);
        return response.Products.ToList();
    }

    public async Task<ProductResponse> CreateProductAsync(
        string name,
        double price)
    {
        var request = new CreateProductRequest
        {
            Name = name,
            Price = price
        };
        
        return await _client.CreateProductAsync(request);
    }

    public async Task StreamProductsAsync()
    {
        var request = new StreamProductsRequest();
        
        using var call = _client.StreamProducts(request);
        
        await foreach (var product in call.ResponseStream.ReadAllAsync())
        {
            Console.WriteLine($"Received: {product.Name}");
        }
    }
}

Python Client

import grpc
from generated import product_service_pb2
from generated import product_service_pb2_grpc

class ProductClient:
    def __init__(self, address):
        self.channel = grpc.insecure_channel(address)
        self.stub = product_service_pb2_grpc.ProductServiceStub(self.channel)
    
    def get_product(self, product_id):
        request = product_service_pb2.GetProductRequest(id=product_id)
        return self.stub.GetProduct(request)
    
    def list_products(self, page_size=100):
        request = product_service_pb2.ListProductsRequest(
            page_size=page_size,
            page_number=1
        )
        return self.stub.ListProducts(request)
    
    def stream_products(self):
        request = product_service_pb2.StreamProductsRequest()
        for product in self.stub.StreamProducts(request):
            print(f"Received: {product.name}")

# Usage
client = ProductClient('localhost:5001')
product = client.get_product('123')
print(f"Product: {product.name}")

Authentication

JWT Authentication

services.AddGrpc(options =>
{
    options.Interceptors.Add<GrpcAuthenticationInterceptor>();
});

public class GrpcAuthenticationInterceptor : Interceptor
{
    private readonly ICurrentUserService _currentUserService;

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        if (!_currentUserService.IsAuthenticated)
        {
            throw new RpcException(
                new Status(StatusCode.Unauthenticated, "Not authenticated"));
        }

        return await continuation(request, context);
    }
}
Client includes token:
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
    metadata.Add("Authorization", $"Bearer {token}");
    return Task.CompletedTask;
});

var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
{
    Credentials = ChannelCredentials.Create(
        new SslCredentials(),
        credentials)
});

Error Handling

Status Codes

gRPC uses standard status codes:
CodeDescriptionUse Case
OKSuccessSuccessful operation
CANCELLEDOperation cancelledClient cancelled
INVALID_ARGUMENTInvalid inputValidation failure
NOT_FOUNDResource not foundEntity doesn’t exist
ALREADY_EXISTSResource existsDuplicate creation
PERMISSION_DENIEDNo permissionAuthorization failure
UNAUTHENTICATEDNot authenticatedMissing/invalid token
RESOURCE_EXHAUSTEDRate limitedToo many requests
UNIMPLEMENTEDNot implementedMethod not available
INTERNALInternal errorServer error
UNAVAILABLEService unavailableTemporary failure

Exception Handling

try
{
    var response = await _client.GetProductAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine("Product not found");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
    Console.WriteLine($"Invalid input: {ex.Status.Detail}");
}
catch (RpcException ex)
{
    Console.WriteLine($"gRPC error: {ex.Status}");
}

Performance Optimization

HTTP/2 Settings

{
  "Kestrel": {
    "Endpoints": {
      "Grpc": {
        "Url": "http://localhost:5001",
        "Protocols": "Http2"
      }
    }
  }
}

Message Size Limits

services.AddGrpc(options =>
{
    options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16 MB
    options.MaxSendMessageSize = 16 * 1024 * 1024; // 16 MB
});

Compression

services.AddGrpc(options =>
{
    options.ResponseCompressionLevel = CompressionLevel.Optimal;
});

Testing

Unit Tests

[Fact]
public async Task GetProduct_ValidId_ReturnsProduct()
{
    // Arrange
    var mediator = new Mock<IMediator>();
    var mapper = new Mock<IMapper>();
    var service = new ProductService(mediator.Object, mapper.Object);
    
    var request = new GetProductRequest { Id = Guid.NewGuid().ToString() };
    var context = TestServerCallContext.Create();

    // Act
    var response = await service.GetProduct(request, context);

    // Assert
    Assert.NotNull(response);
}

Integration Tests

public class GrpcTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public GrpcTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task GetProduct_ReturnsProduct()
    {
        // Arrange
        var client = _factory.CreateGrpcClient<ProductService.ProductServiceClient>();
        var request = new GetProductRequest { Id = Guid.NewGuid().ToString() };

        // Act
        var response = await client.GetProductAsync(request);

        // Assert
        Assert.NotNull(response);
    }
}

Best Practices

  • Use clear, consistent naming
  • Version your proto files
  • Document with comments
  • Use well-known types (Timestamp, Duration, etc.)
  • Use streaming for large datasets
  • Enable compression
  • Set appropriate message size limits
  • Use connection pooling
  • Use appropriate status codes
  • Include detailed error messages
  • Implement proper exception handling
  • Log all errors
  • Always use TLS in production
  • Implement authentication
  • Validate all inputs
  • Use interceptors for cross-cutting concerns

gRPC vs REST

AspectgRPCREST
ProtocolHTTP/2HTTP/1.1
FormatProtocol Buffers (binary)JSON (text)
PerformanceFaster, less bandwidthSlower, more bandwidth
StreamingBuilt-inLimited
Browser SupportLimited (gRPC-Web needed)Full
ToolingCode generationOpenAPI
Human ReadableNoYes
Best ForMicroservices, mobile appsPublic APIs, web apps

Installation

Intent.AspNetCore.Grpc

Dependencies

  • Intent.Common.CSharp
  • Intent.Modelers.Services
  • Intent.OutputManager.RoslynWeaver

Next Steps

Controllers

Compare with REST API controllers

MediatR

Dispatch gRPC calls to handlers

Security

Secure gRPC services

Build docs developers (and LLMs) love