# gRPC Streaming Clients Build gRPC clients for event streaming in multiple languages. ## Overview gRPC event streaming clients support multiple programming languages: - **C# / .NET** - Native gRPC support via Grpc.Net.Client - **TypeScript / Node.js** - @grpc/grpc-js package - **Go** - google.golang.org/grpc - **Python** - grpcio package ## C# / .NET Client ### Installation ```bash dotnet add package Grpc.Net.Client dotnet add package Google.Protobuf dotnet add package Grpc.Tools ``` ### Basic Client ```csharp using Grpc.Net.Client; using Svrnty.CQRS.Events.Grpc; // Create channel using var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new EventStreamService.EventStreamServiceClient(channel); // Subscribe to persistent stream using var call = client.SubscribeToPersistent(); await call.RequestStream.WriteAsync(new PersistentSubscriptionRequest { StreamName = "orders", StartOffset = 0, SubscriptionId = Guid.NewGuid().ToString() }); await foreach (var @event in call.ResponseStream.ReadAllAsync()) { Console.WriteLine($"{@event.EventType}: {@event.EventId}"); } ``` ### Production Client ```csharp public class EventStreamGrpcClient : IDisposable { private readonly GrpcChannel _channel; private readonly EventStreamService.EventStreamServiceClient _client; public EventStreamGrpcClient(string address) { _channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions { MaxReceiveMessageSize = 10 * 1024 * 1024, // 10 MB MaxSendMessageSize = 10 * 1024 * 1024, Credentials = ChannelCredentials.SecureSsl }); _client = new EventStreamService.EventStreamServiceClient(_channel); } public async Task SubscribeAsync( string streamName, long startOffset, Func handler, CancellationToken ct) { using var call = _client.SubscribeToPersistent(cancellationToken: ct); await call.RequestStream.WriteAsync(new PersistentSubscriptionRequest { StreamName = streamName, StartOffset = startOffset, SubscriptionId = Guid.NewGuid().ToString() }); await foreach (var @event in call.ResponseStream.ReadAllAsync(ct)) { await handler(@event); } } public void Dispose() { _channel?.Dispose(); } } // Usage using var client = new EventStreamGrpcClient("https://event-store.example.com"); await client.SubscribeAsync( "orders", startOffset: 0, handler: async @event => { Console.WriteLine($"Received: {@event.EventType}"); await ProcessEventAsync(@event); }, ct); ``` ## TypeScript / Node.js Client ### Installation ```bash npm install @grpc/grpc-js @grpc/proto-loader npm install --save-dev @types/node ``` ### Proto Loading ```typescript import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import path from 'path'; // Load proto file const PROTO_PATH = path.join(__dirname, '../protos/event_stream.proto'); const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }); const eventStreamProto = grpc.loadPackageDefinition(packageDefinition).svrnty.cqrs.events as any; // Create client const client = new eventStreamProto.EventStreamService( 'localhost:5001', grpc.credentials.createInsecure() ); ``` ### Subscription Client ```typescript interface StreamEvent { event_id: string; event_type: string; stream_name: string; offset: number; timestamp: { seconds: number; nanos: number }; data: string; metadata: Record; } async function subscribe( streamName: string, startOffset: number, handler: (event: StreamEvent) => Promise ): Promise { const call = client.subscribeToPersistent(); // Send subscription request call.write({ stream_name: streamName, start_offset: startOffset, subscription_id: crypto.randomUUID() }); // Receive events call.on('data', async (event: StreamEvent) => { try { await handler(event); } catch (error) { console.error('Error processing event:', error); } }); call.on('error', (error: Error) => { console.error('Stream error:', error); }); call.on('end', () => { console.log('Stream ended'); }); // Keep call alive return new Promise((resolve, reject) => { call.on('error', reject); call.on('end', resolve); }); } // Usage await subscribe('orders', 0, async (event) => { console.log(`${event.event_type}: ${event.event_id}`); const data = JSON.parse(event.data); await processEvent(data); }); ``` ## Go Client ### Installation ```bash go get google.golang.org/grpc go get google.golang.org/protobuf/proto ``` ### Generate Code ```bash protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ event_stream.proto ``` ### Subscription Client ```go package main import ( "context" "fmt" "io" "log" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "github.com/your-org/event-stream/proto" ) type EventHandler func(*pb.StreamEventProto) error func Subscribe( address string, streamName string, startOffset int64, handler EventHandler, ) error { // Connect conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("failed to connect: %w", err) } defer conn.Close() client := pb.NewEventStreamServiceClient(conn) ctx := context.Background() // Create stream stream, err := client.SubscribeToPersistent(ctx) if err != nil { return fmt.Errorf("failed to subscribe: %w", err) } // Send subscription request err = stream.Send(&pb.PersistentSubscriptionRequest{ StreamName: streamName, StartOffset: startOffset, SubscriptionId: uuid.New().String(), }) if err != nil { return fmt.Errorf("failed to send request: %w", err) } // Receive events for { event, err := stream.Recv() if err == io.EOF { break } if err != nil { return fmt.Errorf("failed to receive: %w", err) } if err := handler(event); err != nil { log.Printf("Error processing event %s: %v", event.EventId, err) } } return nil } // Usage func main() { err := Subscribe( "localhost:5001", "orders", 0, func(event *pb.StreamEventProto) error { fmt.Printf("%s: %s\n", event.EventType, event.EventId) return processEvent(event) }, ) if err != nil { log.Fatal(err) } } ``` ## Python Client ### Installation ```bash pip install grpcio grpcio-tools ``` ### Generate Code ```bash python -m grpc_tools.protoc \ -I. \ --python_out=. \ --grpc_python_out=. \ event_stream.proto ``` ### Subscription Client ```python import grpc import uuid from event_stream_pb2 import PersistentSubscriptionRequest from event_stream_pb2_grpc import EventStreamServiceStub class EventStreamClient: def __init__(self, address: str): self.channel = grpc.insecure_channel(address) self.client = EventStreamServiceStub(self.channel) def subscribe( self, stream_name: str, start_offset: int, handler ): def request_iterator(): yield PersistentSubscriptionRequest( stream_name=stream_name, start_offset=start_offset, subscription_id=str(uuid.uuid4()) ) responses = self.client.SubscribeToPersistent(request_iterator()) for event in responses: try: handler(event) except Exception as e: print(f"Error processing event {event.event_id}: {e}") def close(self): self.channel.close() # Usage client = EventStreamClient('localhost:5001') try: client.subscribe( 'orders', 0, lambda event: print(f"{event.event_type}: {event.event_id}") ) finally: client.close() ``` ### Async Client ```python import asyncio import grpc.aio from event_stream_pb2 import PersistentSubscriptionRequest from event_stream_pb2_grpc import EventStreamServiceStub class AsyncEventStreamClient: def __init__(self, address: str): self.channel = grpc.aio.insecure_channel(address) self.client = EventStreamServiceStub(self.channel) async def subscribe( self, stream_name: str, start_offset: int, handler ): async def request_iterator(): yield PersistentSubscriptionRequest( stream_name=stream_name, start_offset=start_offset, subscription_id=str(uuid.uuid4()) ) call = self.client.SubscribeToPersistent(request_iterator()) async for event in call: try: await handler(event) except Exception as e: print(f"Error processing event {event.event_id}: {e}") async def close(self): await self.channel.close() # Usage async def main(): client = AsyncEventStreamClient('localhost:5001') try: await client.subscribe( 'orders', 0, lambda event: print(f"{event.event_type}: {event.event_id}") ) finally: await client.close() asyncio.run(main()) ``` ## Authentication ### C# with Bearer Token ```csharp var credentials = CallCredentials.FromInterceptor((context, metadata) => { metadata.Add("Authorization", $"Bearer {accessToken}"); return Task.CompletedTask; }); var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { Credentials = ChannelCredentials.Create( new SslCredentials(), credentials) }); ``` ### TypeScript with Metadata ```typescript const metadata = new grpc.Metadata(); metadata.add('authorization', `Bearer ${accessToken}`); const call = client.subscribeToPersistent(metadata); ``` ### Go with Interceptor ```go func authInterceptor(token string) grpc.UnaryClientInterceptor { return func( ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption, ) error { ctx = metadata.AppendToOutgoingContext(ctx, "authorization", fmt.Sprintf("Bearer %s", token)) return invoker(ctx, method, req, reply, cc, opts...) } } conn, err := grpc.Dial( address, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), grpc.WithUnaryInterceptor(authInterceptor(token)), ) ``` ### Python with Credentials ```python call_credentials = grpc.access_token_call_credentials(access_token) channel_credentials = grpc.ssl_channel_credentials() composite_credentials = grpc.composite_channel_credentials( channel_credentials, call_credentials ) channel = grpc.secure_channel('localhost:5001', composite_credentials) ``` ## Best Practices ### ✅ DO - Use secure channels in production (TLS) - Implement reconnection logic - Handle errors gracefully - Use async/await patterns - Close channels properly - Use appropriate timeouts - Implement authentication - Log connection lifecycle ### ❌ DON'T - Don't use insecure channels in production - Don't ignore connection errors - Don't block event processing - Don't leak resources (unclosed channels) - Don't use very short timeouts - Don't skip authentication - Don't ignore cancellation - Don't forget error handling ## See Also - [gRPC Streaming Overview](README.md) - [Persistent Subscriptions](persistent-subscriptions.md) - [Queue Subscriptions](queue-subscriptions.md) - [gRPC Integration](../../grpc-integration/README.md) - [Event Streaming Overview](../README.md)