dotnet-cqrs/docs/grpc-integration/grpc-clients.md

8.4 KiB

gRPC Clients

Building clients to consume gRPC services.

C# Client

Installation

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Generate Client Code

.csproj:

<ItemGroup>
  <Protobuf Include="Protos\cqrs_services.proto" GrpcServices="Client" />
</ItemGroup>

Basic Client

using Grpc.Net.Client;
using MyApp.Grpc;

// Create channel
var channel = GrpcChannel.ForAddress("https://localhost:5001");

// Create clients
var commandClient = new CommandService.CommandServiceClient(channel);
var queryClient = new QueryService.QueryServiceClient(channel);

// Call CreateUser
var createResponse = await commandClient.CreateUserAsync(new CreateUserCommand
{
    Name = "John Doe",
    Email = "john@example.com"
});

Console.WriteLine($"Created user: {createResponse.UserId}");

// Call GetUser
var user = await queryClient.GetUserAsync(new GetUserQuery
{
    UserId = createResponse.UserId
});

Console.WriteLine($"User: {user.Name}, {user.Email}");

// Cleanup
await channel.ShutdownAsync();

With Error Handling

using Grpc.Core;
using Google.Rpc;

try
{
    var user = await queryClient.GetUserAsync(new GetUserQuery { UserId = 999 });
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine("User not found");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
{
    var status = ex.GetRpcStatus();
    var badRequest = status.GetDetail<BadRequest>();

    foreach (var violation in badRequest.FieldViolations)
    {
        Console.WriteLine($"{violation.Field}: {violation.Description}");
    }
}
catch (RpcException ex)
{
    Console.WriteLine($"gRPC error: {ex.Status}");
}

With Deadlines

var deadline = DateTime.UtcNow.AddSeconds(5);

var user = await queryClient.GetUserAsync(
    new GetUserQuery { UserId = 42 },
    deadline: deadline);

With Metadata

var metadata = new Metadata
{
    { "Authorization", "Bearer token..." },
    { "X-Request-ID", Guid.NewGuid().ToString() }
};

var user = await queryClient.GetUserAsync(
    new GetUserQuery { UserId = 42 },
    headers: metadata);

TypeScript Client (grpc-web)

Installation

npm install grpc-web
npm install google-protobuf
npm install --save-dev @types/google-protobuf

Generate Code

protoc -I=. cqrs_services.proto \
  --js_out=import_style=commonjs:. \
  --grpc-web_out=import_style=typescript,mode=grpcwebtext:.

Basic Client

import { CommandServiceClient } from './cqrs_services_grpc_web_pb';
import { CreateUserCommand, GetUserQuery } from './cqrs_services_pb';

const client = new CommandServiceClient('http://localhost:5000');

// Create user
const createRequest = new CreateUserCommand();
createRequest.setName('John Doe');
createRequest.setEmail('john@example.com');

client.createUser(createRequest, {}, (err, response) => {
  if (err) {
    console.error('Error:', err.message);
    return;
  }

  console.log('Created user:', response.getUserId());
});

// With promises
const createUser = async () => {
  const request = new CreateUserCommand();
  request.setName('Jane Doe');
  request.setEmail('jane@example.com');

  try {
    const response = await client.createUser(request, {});
    return response.getUserId();
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
};

Server Configuration for grpc-web

builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowGrpcWeb", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
    });
});

var app = builder.Build();

app.UseGrpcWeb();
app.UseCors("AllowGrpcWeb");

app.MapGrpcService<CommandServiceImpl>().EnableGrpcWeb();
app.MapGrpcService<QueryServiceImpl>().EnableGrpcWeb();

Go Client

Generate Code

protoc --go_out=. --go-grpc_out=. cqrs_services.proto

Basic Client

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    pb "myapp/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:5001", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    cmdClient := pb.NewCommandServiceClient(conn)
    queryClient := pb.NewQueryServiceClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()

    // Create user
    createResp, err := cmdClient.CreateUser(ctx, &pb.CreateUserCommand{
        Name:  "John Doe",
        Email: "john@example.com",
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Created user: %d", createResp.UserId)

    // Get user
    user, err := queryClient.GetUser(ctx, &pb.GetUserQuery{
        UserId: createResp.UserId,
    })
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("User: %s, %s", user.Name, user.Email)
}

Python Client

Installation

pip install grpcio grpcio-tools

Generate Code

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. cqrs_services.proto

Basic Client

import grpc
import cqrs_services_pb2
import cqrs_services_pb2_grpc

channel = grpc.insecure_channel('localhost:5001')

cmd_stub = cqrs_services_pb2_grpc.CommandServiceStub(channel)
query_stub = cqrs_services_pb2_grpc.QueryServiceStub(channel)

# Create user
create_response = cmd_stub.CreateUser(
    cqrs_services_pb2.CreateUserCommand(
        name='John Doe',
        email='john@example.com'
    )
)

print(f'Created user: {create_response.user_id}')

# Get user
user = query_stub.GetUser(
    cqrs_services_pb2.GetUserQuery(user_id=create_response.user_id)
)

print(f'User: {user.name}, {user.email}')

channel.close()

Connection Management

Reusing Channels

// ✅ Good - Reuse channel
public class GrpcClientFactory
{
    private readonly GrpcChannel _channel;

    public GrpcClientFactory(string address)
    {
        _channel = GrpcChannel.ForAddress(address);
    }

    public CommandService.CommandServiceClient CreateCommandClient()
    {
        return new CommandService.CommandServiceClient(_channel);
    }

    public QueryService.QueryServiceClient CreateQueryClient()
    {
        return new QueryService.QueryServiceClient(_channel);
    }

    public async Task ShutdownAsync()
    {
        await _channel.ShutdownAsync();
    }
}

// ❌ Bad - New channel per call
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new CommandService.CommandServiceClient(channel);
await client.CreateUserAsync(command);
await channel.ShutdownAsync();  // Expensive!

Dependency Injection

builder.Services.AddGrpcClient<CommandService.CommandServiceClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
});

builder.Services.AddGrpcClient<QueryService.QueryServiceClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
});

// Usage in service
public class UserService
{
    private readonly CommandService.CommandServiceClient _commandClient;
    private readonly QueryService.QueryServiceClient _queryClient;

    public UserService(
        CommandService.CommandServiceClient commandClient,
        QueryService.QueryServiceClient queryClient)
    {
        _commandClient = commandClient;
        _queryClient = queryClient;
    }

    public async Task<int> CreateUserAsync(string name, string email)
    {
        var response = await _commandClient.CreateUserAsync(new CreateUserCommand
        {
            Name = name,
            Email = email
        });

        return response.UserId;
    }
}

Best Practices

DO

  • Reuse GrpcChannel instances
  • Use dependency injection for clients
  • Set appropriate deadlines
  • Handle errors appropriately
  • Use metadata for tracing
  • Close channels when done
  • Use connection pooling

DON'T

  • Don't create new channels per request
  • Don't ignore exceptions
  • Don't skip deadlines
  • Don't hardcode server addresses
  • Don't forget to dispose channels

See Also