dotnet-cqrs/docs/event-streaming/grpc-streaming/grpc-clients.md

525 lines
12 KiB
Markdown

# 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<StreamEventProto, Task> 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<string, string>;
}
async function subscribe(
streamName: string,
startOffset: number,
handler: (event: StreamEvent) => Promise<void>
): Promise<void> {
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)