Demo application: MeterReader (IoT meter-reading service) ASP.NET Core / .NET 10, Visual Studio
Table of Contents
1. What is gRPC?
1.1 History of Distributed APIs
┌─────────────────────────────────────────────────────────────────┐
│ EVOLUTION OF DISTRIBUTED APIS │
│ │
│ 1990s: RPC (Remote Procedure Call) │
│ → Remote function calls, tight coupling │
│ │
│ 2000s: CORBA / DCOM / RMI │
│ → Complexity, interoperability issues │
│ │
│ 2000s-2010s: SOAP / XML Web Services │
│ → WSDL contracts, verbose XML, slow │
│ │
│ 2010s+: REST + JSON │
│ → Simple, HTTP/1.1, human-readable, ubiquitous │
│ │
│ 2010s+: GraphQL │
│ → Flexible queries, over/under-fetching solved │
│ │
│ Since 2016: gRPC (Google) │
│ → High performance, Protocol Buffers, HTTP/2 │
│ → Strict contracts, multi-language │
└─────────────────────────────────────────────────────────────────┘
1.2 gRPC and Protocol Buffers
gRPC = g Remote Procedure Call (Google open-source framework, 2016)
Protocol Buffers (Protobuf v3) = Interface Definition Language (IDL)
- Binary serialization (faster and smaller than JSON/XML)
- Language-neutral (generates code for .NET, Python, Java, Go, etc.)
- Mandatory contract defined in a
.protofile
REST vs gRPC Comparison:
| Criterion | REST | gRPC |
|---|---|---|
| Format | JSON (text) | Protobuf (binary) |
| Transport | HTTP/1.1 or 2 | HTTP/2 required |
| Contract | Optional (OpenAPI) | Required (.proto) |
| Streaming | SSE or WebSocket | Native (4 types) |
| Browser | Yes | Partial (gRPC-Web) |
| Performance | Moderate | Very high |
| Readability | Human-readable | Not readable |
1.3 When to Use gRPC?
✅ Appropriate use cases:
- Intra-datacenter communication (microservices)
- IoT systems (small binary payloads)
- Real-time streaming
- Polyglot APIs (multiple client languages)
- High-performance requirements (millions of requests/sec)
❌ Inappropriate use cases:
- Public APIs for web browsers (use gRPC-Web or REST)
- Third-party partner integrations (REST + OpenAPI more universal)
- Manual debugging (binary, not human-readable)
2. Adding gRPC to ASP.NET Core
2.1 Server Configuration
// Program.cs — gRPC Server
var builder = WebApplication.CreateBuilder(args);
// Add gRPC services
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
// gRPC requires HTTP/2
// In appsettings.json or Kestrel config:
// "Kestrel": { "EndpointDefaults": { "Protocols": "Http2" } }
var app = builder.Build();
// Register gRPC services
app.MapGrpcService<MeterReadingService>();
// Information page for normal HTTP requests
app.MapGet("/", () => "gRPC server running. Use a gRPC client.");
app.Run();
Required NuGet: Grpc.AspNetCore
2.2 .proto Files
// Protos/meter_reading.proto
syntax = "proto3";
option csharp_namespace = "MeterReader.Grpc";
package meter_reader;
// Message definitions (data types)
message ReadingPacket {
CustomerInfo customer = 1; // Field #1
repeated Reading readings = 2; // List of readings (repeated = collection)
ReadingStatus status = 3;
}
message CustomerInfo {
int32 customer_id = 1;
}
message Reading {
google.protobuf.Timestamp reading_time = 1; // Well-known type
int32 customer_id = 2;
double value = 3;
ReadingType type = 4;
}
// Enum
enum ReadingStatus {
READING_STATUS_UNKNOWN = 0; // Required default value (= 0)
READING_STATUS_SUCCESS = 1;
READING_STATUS_FAILURE = 2;
}
enum ReadingType {
READING_TYPE_GAS = 0;
READING_TYPE_ELECTRIC = 1;
READING_TYPE_WATER = 2;
}
message StatusMessage {
bool success = 1;
}
// Service definition (RPC methods)
service MeterReadingService {
// Unary call (request → response)
rpc SendReading (ReadingPacket) returns (StatusMessage);
// Server-side streaming
rpc GetMeterHistory (CustomerInfo) returns (stream Reading);
// Client-side streaming
rpc BulkSendReadings (stream Reading) returns (StatusMessage);
// Bidirectional streaming
rpc StreamReadings (stream ReadingPacket) returns (stream StatusMessage);
}
Project .csproj Configuration:
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<!-- Include proto file (server) -->
<Protobuf Include="Protos\meter_reading.proto" GrpcServices="Server" />
<!-- Client only -->
<!-- <Protobuf Include="Protos\meter_reading.proto" GrpcServices="Client" /> -->
<!-- Both -->
<!-- <Protobuf Include="Protos\meter_reading.proto" GrpcServices="Both" /> -->
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.*" />
<PackageReference Include="Google.Protobuf" Version="3.*" />
</ItemGroup>
</Project>
2.3 Service Implementation
// Services/MeterReadingService.cs
using Grpc.Core;
using MeterReader.Grpc; // Generated by Protobuf compiler
public class MeterReadingService : MeterReadingService.MeterReadingServiceBase
{
private readonly ILogger<MeterReadingService> _logger;
private readonly IMeterReadingRepository _repository;
public MeterReadingService(
ILogger<MeterReadingService> logger,
IMeterReadingRepository repository)
{
_logger = logger;
_repository = repository;
}
// Unary method — overrides the generated method
public override async Task<StatusMessage> SendReading(
ReadingPacket request,
ServerCallContext context)
{
_logger.LogInformation(
"Received {Count} readings for customer {CustomerId}",
request.Readings.Count, request.Customer.CustomerId);
try
{
foreach (var reading in request.Readings)
{
await _repository.SaveReadingAsync(new MeterReading
{
CustomerId = reading.CustomerId,
Value = reading.Value,
ReadingTime = reading.ReadingTime.ToDateTime(),
Type = (ReadingType)reading.Type
});
}
return new StatusMessage { Success = true };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving readings");
return new StatusMessage { Success = false };
}
}
// Server-side streaming
public override async Task GetMeterHistory(
CustomerInfo request,
IServerStreamWriter<Reading> responseStream,
ServerCallContext context)
{
var readings = await _repository.GetReadingsForCustomerAsync(request.CustomerId);
foreach (var reading in readings)
{
// Check if the client has cancelled
context.CancellationToken.ThrowIfCancellationRequested();
await responseStream.WriteAsync(new Reading
{
CustomerId = reading.CustomerId,
Value = reading.Value,
ReadingTime = Timestamp.FromDateTime(reading.ReadingTime)
});
await Task.Delay(100); // Simulate realistic delay
}
}
}
3. gRPC Clients
3.1 .NET Client (Worker Service)
// MeterReader.Client/Worker.cs — .NET Worker Service
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly MeterReadingService.MeterReadingServiceClient _client;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
// Create channel and client
var channel = GrpcChannel.ForAddress("https://localhost:5001");
_client = new MeterReadingService.MeterReadingServiceClient(channel);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Create reading packet
var packet = new ReadingPacket
{
Customer = new CustomerInfo { CustomerId = 54321 },
Status = ReadingStatus.Success
};
// Add readings
packet.Readings.Add(new Reading
{
CustomerId = 54321,
Value = 98.7,
Type = ReadingType.Electric,
ReadingTime = Timestamp.FromDateTime(DateTime.UtcNow)
});
try
{
var result = await _client.SendReadingAsync(packet);
_logger.LogInformation("Reading sent. Success: {Success}", result.Success);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
_logger.LogError("Authentication failed: {Detail}", ex.Status.Detail);
}
catch (RpcException ex)
{
_logger.LogError("gRPC error: {StatusCode} — {Detail}",
ex.StatusCode, ex.Status.Detail);
}
await Task.Delay(5000, stoppingToken); // Send every 5 seconds
}
}
}
Program.cs for the Worker:
// MeterReader.Client/Program.cs
var builder = Host.CreateApplicationBuilder(args);
// Register the gRPC service as a typed client
builder.Services.AddGrpcClient<MeterReadingService.MeterReadingServiceClient>(options =>
{
options.Address = new Uri("https://localhost:5001");
});
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
3.2 Python Client
# Install packages
pip install grpcio grpcio-tools
# Generate Python stubs from the .proto file
python -m grpc_tools.protoc \
-I./Protos \
--python_out=. \
--grpc_python_out=. \
./Protos/meter_reading.proto
# meter_reader_client.py
import grpc
import meter_reading_pb2 as pb
import meter_reading_pb2_grpc as pb_grpc
from google.protobuf.timestamp_pb2 import Timestamp
from datetime import datetime
def run():
# Unsecured connection (dev only)
channel = grpc.insecure_channel('localhost:5000')
# Secure TLS channel
# credentials = grpc.ssl_channel_credentials()
# channel = grpc.secure_channel('localhost:5001', credentials)
stub = pb_grpc.MeterReadingServiceStub(channel)
# Create a timestamp
now = datetime.utcnow()
ts = Timestamp()
ts.FromDatetime(now)
# Send a reading
reading = pb.Reading(
customer_id=54321,
value=102.5,
type=pb.READING_TYPE_ELECTRIC,
reading_time=ts
)
packet = pb.ReadingPacket(
customer=pb.CustomerInfo(customer_id=54321),
status=pb.READING_STATUS_SUCCESS,
readings=[reading]
)
response = stub.SendReading(packet)
print(f"Sent successfully: {response.success}")
if __name__ == '__main__':
run()
3.3 JavaScript Client with gRPC-Web
Limitation: browsers do not support HTTP/2 in “raw” mode → use gRPC-Web.
# Install npm packages
npm install grpc-web google-protobuf
# Generate JavaScript stubs
protoc \
--js_out=import_style=commonjs,binary:./src \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./src \
meter_reading.proto
// Program.cs — Add gRPC-Web middleware on the server side
builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding");
});
});
app.UseCors("AllowAll");
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.MapGrpcService<MeterReadingService>()
.EnableGrpcWeb()
.RequireCors("AllowAll");
// meter_reader_client.js
const { MeterReadingServiceClient } = require('./meter_reading_grpc_web_pb');
const { ReadingPacket, Reading, CustomerInfo, ReadingStatus, ReadingType } = require('./meter_reading_pb');
const { Timestamp } = require('google-protobuf/google/protobuf/timestamp_pb');
const client = new MeterReadingServiceClient('http://localhost:5000');
const packet = new ReadingPacket();
const customer = new CustomerInfo();
customer.setCustomerId(54321);
packet.setCustomer(customer);
packet.setStatus(ReadingStatus.READING_STATUS_SUCCESS);
const reading = new Reading();
reading.setCustomerId(54321);
reading.setValue(102.5);
reading.setType(ReadingType.READING_TYPE_ELECTRIC);
const ts = new Timestamp();
ts.fromDate(new Date());
reading.setReadingTime(ts);
packet.addReadings(reading);
client.sendReading(packet, {}, (err, response) => {
if (err) {
console.error('gRPC Error:', err);
} else {
console.log('Success:', response.getSuccess());
}
});
3.4 Streaming
// .NET Client — Server-side streaming
public async Task StreamHistoryAsync(int customerId)
{
var customerInfo = new CustomerInfo { CustomerId = customerId };
using var call = _client.GetMeterHistory(customerInfo);
await foreach (var reading in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Reading: {reading.Value} at {reading.ReadingTime}");
}
}
// .NET Client — Client-side streaming
public async Task BulkSendReadingsAsync(IEnumerable<ReadingPacket> packets)
{
using var call = _client.BulkSendReadings();
foreach (var packet in packets)
{
await call.RequestStream.WriteAsync(
packet.Readings.First()); // Send reading by reading
}
await call.RequestStream.CompleteAsync();
var result = await call; // Wait for the final response
Console.WriteLine($"Bulk send success: {result.Success}");
}
// .NET Client — Bidirectional streaming
public async Task StreamBidirectionalAsync()
{
using var call = _client.StreamReadings();
// Read responses in the background
var responseTask = Task.Run(async () =>
{
await foreach (var status in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Server response: {status.Success}");
}
});
// Send packets
for (int i = 0; i < 10; i++)
{
await call.RequestStream.WriteAsync(CreatePacket(i));
await Task.Delay(1000);
}
await call.RequestStream.CompleteAsync();
await responseTask;
}
4. Securing gRPC
Important note: gRPC is simply ASP.NET Core middleware → all ASP.NET Core security applies identically.
4.1 JWT Bearer Authentication
// Program.cs — Server: configure JWT auth
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001"; // IDP
options.Audience = "meterreaderapi";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true
};
});
builder.Services.AddAuthorization();
// In the pipeline
app.UseAuthentication();
app.UseAuthorization();
// On the gRPC service
app.MapGrpcService<MeterReadingService>();
// Services/MeterReadingService.cs — Protect the service
[Authorize] // Requires authentication for all methods
public class MeterReadingService : MeterReadingService.MeterReadingServiceBase
{
public override async Task<StatusMessage> SendReading(
ReadingPacket request, ServerCallContext context)
{
// Access user claims
var userId = context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier);
// ...
}
}
// .NET Client — Send the JWT in metadata
public async Task SendReadingWithAuthAsync(ReadingPacket packet, string accessToken)
{
// Option 1: per request
var headers = new Metadata
{
{ "Authorization", $"Bearer {accessToken}" }
};
var result = await _client.SendReadingAsync(packet, headers);
// Option 2: for all channel requests
// GrpcChannelOptions.Credentials with CallCredentials
}
// Authentication error handling
try
{
var result = await _client.SendReadingAsync(packet);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unauthenticated)
{
// Refresh the token or redirect to login
await RefreshTokenAsync();
}
4.2 Certificate Authentication
# Generate self-signed certificates for development
# PowerShell script: makecerts.ps1
# Root certificate (CA)
$rootCert = New-SelfSignedCertificate `
-CertStoreLocation cert:\CurrentUser\My `
-DnsName "GloboTicketCA" `
-KeyUsage CertSign, CRLSign `
-KeyLength 4096 `
-HashAlgorithm SHA256
# Client certificate signed by the CA
$clientCert = New-SelfSignedCertificate `
-CertStoreLocation cert:\CurrentUser\My `
-DnsName "MeterReaderClient" `
-Signer $rootCert `
-KeyLength 2048
# Export the client certificate
Export-PfxCertificate -Cert $clientCert -FilePath "client.pfx" -Password (Read-Host -AsSecureString)
// Program.cs — Server: certificate authentication
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck; // Dev only
options.ValidateCertificateUse = false; // Dev only
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Additional validation or claim creation
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject),
new Claim(ClaimTypes.Name, context.ClientCertificate.GetNameInfo(
X509NameType.SimpleName, false))
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
};
});
// Kestrel: configure TLS with client certificate validation
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
// .NET Client — Send the client certificate
public class MeterReaderGrpcClient
{
private readonly MeterReadingService.MeterReadingServiceClient _client;
public MeterReaderGrpcClient(string serverUrl, string certPath, string certPassword)
{
// Load the client certificate
var clientCert = new X509Certificate2(certPath, certPassword);
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCert);
// Accept self-signed certificates (DEV ONLY)
handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var channel = GrpcChannel.ForAddress(serverUrl, new GrpcChannelOptions
{
HttpHandler = handler
});
_client = new MeterReadingService.MeterReadingServiceClient(channel);
}
}
Reference — Protocol Buffer Types
| Protobuf Type | .NET Type | Python Type |
|---|---|---|
int32 | int | int |
int64 | long | int |
float | float | float |
double | double | float |
bool | bool | bool |
string | string | str |
bytes | ByteString | bytes |
repeated T | RepeatedField<T> | list |
map<K, V> | MapField<K, V> | dict |
google.protobuf.Timestamp | Timestamp | Timestamp |
google.protobuf.Duration | Duration | Duration |
gRPC Streaming Types
| Type | Proto | Client | Server | Usage |
|---|---|---|---|---|
| Unary | rpc Method(Req) returns (Resp) | One message | One message | Standard requests |
| Server streaming | rpc Method(Req) returns (stream Resp) | One message | Multiple messages | History, events |
| Client streaming | rpc Method(stream Req) returns (Resp) | Multiple messages | One message | Bulk upload |
| Bidirectional | rpc Method(stream Req) returns (stream Resp) | Multiple messages | Multiple messages | Chat, real-time |
7. gRPC Architecture — Deep Dive
7.1 gRPC vs REST vs GraphQL Comparison
graph TD
subgraph "API Choices"
REST["REST/JSON\n- Web standards\n- Human-readable\n- Easy to debug\n- Ideal: public APIs, web"]
GRPC["gRPC (Protobuf)\n- High performance\n- Strict contracts\n- Bidirectional streaming\n- Ideal: microservices, internal"]
GQL["GraphQL\n- Flexible queries\n- Client chooses fields\n- Ideal: mobile/SPA APIs"]
SignalR["SignalR\n- Real-time\n- WebSockets\n- Ideal: server push"]
end
| Criterion | REST/JSON | gRPC | GraphQL | SignalR |
|---|---|---|---|---|
| Format | JSON (text) | Protobuf (binary) | JSON | JSON/binary |
| Performance | Average | Very high | Average | High |
| Contracts | OpenAPI/Swagger | Required (.proto) | Schema | N/A |
| Streaming | Not native | Yes (bidirectional) | Subscriptions | Yes |
| Browser support | ✅ Native | ⚠️ Via gRPC-Web | ✅ Native | ✅ Native |
| Languages | All | All | All | .NET/JS |
| Readability | ✅ JSON | ❌ Binary | ✅ JSON | ✅ JSON |
| Debugging | Easy (Postman) | Medium (Postman/grpcurl) | Easy | Difficult |
7.2 gRPC Communication Flow
sequenceDiagram
participant Client as Client .NET/Python/JS
participant Proto as Protobuf Compiler
participant Server as ASP.NET Core gRPC Server
Note over Proto: One-time .proto compilation
Proto->>Client: Generates MeterReaderServiceClient.cs
Proto->>Server: Generates MeterReaderServiceBase.cs
Note over Client,Server: gRPC call (HTTP/2)
Client->>Server: POST /MeterReaderService/AddReading
Note right of Client: Binary (Protobuf) in body
Server->>Server: Execute business logic
Server-->>Client: Response (ReadingStatus)
Note left of Server: Binary (Protobuf) in response
8. Protocol Buffer (.proto) Files — Complete Guide
8.1 Complete proto3 Syntax
// meterservice.proto
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option csharp_namespace = "MeterReader.gRpc";
// --- MESSAGES ---
// Meter reading message
message ReadingMessage {
int32 customer_id = 1; // Tag number 1 — NEVER change
google.protobuf.Timestamp reading_time = 2;
int32 reading_value = 3;
string notes = 4; // Optional field (empty string by default)
}
// Packet containing multiple readings
message ReadingPacket {
ReadingStatus status = 1; // Enum
repeated ReadingMessage readings = 2; // List/array
// Oneof — only one of the two can be set
oneof result {
string success_message = 3;
string error_message = 4;
}
}
// Response message
message StatusMessage {
bool success = 1;
string message = 2;
int32 reading_count = 3;
}
// Authentication token
message TokenRequest {
string username = 1;
string password = 2;
}
message TokenResponse {
string token = 1;
bool success = 2;
}
// Error message for streaming
message ErrorMessage {
string message = 1;
ErrorSeverity severity = 2;
}
// --- ENUMERATIONS ---
// Reading status
enum ReadingStatus {
READING_STATUS_UNSPECIFIED = 0; // Required default value = 0
SUCCESS = 1;
FAIL = 2;
}
enum ErrorSeverity {
ERROR_SEVERITY_UNSPECIFIED = 0;
INFO = 1;
WARNING = 2;
ERROR = 3;
CRITICAL = 4;
}
// --- SERVICE ---
service MeterReaderService {
// Unary RPC — single request/response
rpc AddReading (ReadingPacket) returns (StatusMessage);
// Server-side streaming — one message, multiple responses
rpc GetReadings (google.protobuf.Empty) returns (stream ReadingMessage);
// Client-side streaming — multiple messages, one response
rpc AddReadings (stream ReadingPacket) returns (StatusMessage);
// Bidirectional streaming — multiple messages in both directions
rpc ProcessReadings (stream ReadingPacket) returns (stream ErrorMessage);
// Authentication
rpc GetToken (TokenRequest) returns (TokenResponse);
}
8.2 Protobuf Types vs C# Types
| Protobuf Type | C# Type | Example |
|---|---|---|
double | double | 3.14 |
float | float | 3.14f |
int32 | int | 42 |
int64 | long | 9999999999 |
uint32 | uint | 42u |
uint64 | ulong | 9999999999ul |
bool | bool | true |
string | string | "hello" |
bytes | ByteString | binary |
google.protobuf.Timestamp | Timestamp → DateTime | Timestamp.FromDateTime(DateTime.UtcNow) |
google.protobuf.Duration | Duration | duration |
google.protobuf.Empty | Empty | empty message |
repeated T | RepeatedField<T> | list |
map<K, V> | MapField<K, V> | dictionary |
9. Complete gRPC Service Implementation
9.1 Complete ASP.NET Core Service
// Services/MeterReadingService.cs
using Grpc.Core;
using MeterReader.gRpc;
using Microsoft.AspNetCore.Authorization;
namespace MeterReader.Services;
public class MeterReadingService : MeterReaderService.MeterReaderServiceBase
{
private readonly ILogger<MeterReadingService> _logger;
private readonly IMeterReadingRepository _repository;
private readonly ITokenService _tokenService;
public MeterReadingService(
ILogger<MeterReadingService> logger,
IMeterReadingRepository repository,
ITokenService tokenService)
{
_logger = logger;
_repository = repository;
_tokenService = tokenService;
}
// Unary RPC — client sends a packet, receives a status
[Authorize]
public override async Task<StatusMessage> AddReading(
ReadingPacket request,
ServerCallContext context)
{
_logger.LogInformation(
"AddReading called with {ReadingCount} readings",
request.Readings.Count);
if (request.Status == ReadingStatus.Fail)
{
_logger.LogWarning("Received failed reading packet");
return new StatusMessage { Success = false, Message = "Packet marked as failed" };
}
try
{
var savedCount = 0;
foreach (var reading in request.Readings)
{
await _repository.SaveReadingAsync(new MeterReading
{
CustomerId = reading.CustomerId,
ReadingTime = reading.ReadingTime.ToDateTimeOffset(),
ReadingValue = reading.ReadingValue
});
savedCount++;
}
_logger.LogInformation("Saved {Count} meter readings", savedCount);
return new StatusMessage
{
Success = true,
Message = $"Saved {savedCount} readings",
ReadingCount = savedCount
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving meter readings");
// Return a standardized gRPC error
throw new RpcException(
new Status(StatusCode.Internal, "Error saving meter readings"));
}
}
// Server-side streaming — client requests, server sends multiple messages
[Authorize]
public override async Task GetReadings(
Empty request,
IServerStreamWriter<ReadingMessage> responseStream,
ServerCallContext context)
{
_logger.LogInformation("GetReadings stream starting");
var readings = await _repository.GetAllReadingsAsync();
foreach (var reading in readings)
{
// Check if the client has cancelled
if (context.CancellationToken.IsCancellationRequested)
{
_logger.LogInformation("GetReadings stream cancelled by client");
break;
}
await responseStream.WriteAsync(new ReadingMessage
{
CustomerId = reading.CustomerId,
ReadingTime = Timestamp.FromDateTimeOffset(reading.ReadingTime),
ReadingValue = reading.ReadingValue
});
// Simulate a delay between messages
await Task.Delay(100, context.CancellationToken);
}
_logger.LogInformation("GetReadings stream completed");
}
// Client-side streaming — client sends multiple messages, receives one response
[Authorize]
public override async Task<StatusMessage> AddReadings(
IAsyncStreamReader<ReadingPacket> requestStream,
ServerCallContext context)
{
var totalCount = 0;
_logger.LogInformation("AddReadings stream starting");
await foreach (var packet in requestStream.ReadAllAsync(context.CancellationToken))
{
foreach (var reading in packet.Readings)
{
await _repository.SaveReadingAsync(new MeterReading
{
CustomerId = reading.CustomerId,
ReadingTime = reading.ReadingTime.ToDateTimeOffset(),
ReadingValue = reading.ReadingValue
});
totalCount++;
}
}
_logger.LogInformation("AddReadings stream completed: {Count} readings saved", totalCount);
return new StatusMessage { Success = true, ReadingCount = totalCount };
}
// Bidirectional streaming — both sides send messages in parallel
[Authorize]
public override async Task ProcessReadings(
IAsyncStreamReader<ReadingPacket> requestStream,
IServerStreamWriter<ErrorMessage> responseStream,
ServerCallContext context)
{
_logger.LogInformation("ProcessReadings bidirectional stream starting");
await foreach (var packet in requestStream.ReadAllAsync(context.CancellationToken))
{
foreach (var reading in packet.Readings)
{
// Validate and send an error message if invalid
if (reading.ReadingValue < 0)
{
await responseStream.WriteAsync(new ErrorMessage
{
Message = $"Invalid reading value {reading.ReadingValue} for customer {reading.CustomerId}",
Severity = ErrorSeverity.Error
});
}
else if (reading.ReadingValue > 10000)
{
await responseStream.WriteAsync(new ErrorMessage
{
Message = $"Suspiciously high reading {reading.ReadingValue}",
Severity = ErrorSeverity.Warning
});
}
else
{
await _repository.SaveReadingAsync(new MeterReading
{
CustomerId = reading.CustomerId,
ReadingTime = reading.ReadingTime.ToDateTimeOffset(),
ReadingValue = reading.ReadingValue
});
}
}
}
}
// JWT generation — public endpoint (no [Authorize])
public override async Task<TokenResponse> GetToken(
TokenRequest request,
ServerCallContext context)
{
_logger.LogInformation("Token request for user {Username}", request.Username);
var token = await _tokenService.GenerateTokenAsync(request.Username, request.Password);
if (token == null)
{
return new TokenResponse { Success = false };
}
return new TokenResponse { Success = true, Token = token };
}
}
9.2 Complete Program.cs Configuration
// Program.cs
using Grpc.AspNetCore.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// gRPC services
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 5 * 1024 * 1024; // 5 MB
options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB
options.Interceptors.Add<LoggingInterceptor>(); // Global interceptor
});
// gRPC-Web support (for JavaScript)
builder.Services.AddGrpcReflection(); // For Postman and similar tools
// JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["JWT:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]!))
};
});
builder.Services.AddAuthorization();
// Business service DI
builder.Services.AddScoped<IMeterReadingRepository, MeterReadingRepository>();
builder.Services.AddScoped<ITokenService, JwtTokenService>();
builder.Services.AddDbContext<MeterReaderContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
// Middleware pipeline — ORDER IS CRITICAL
app.UseRouting();
// gRPC-Web must come BEFORE endpoints
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// gRPC endpoints
app.MapGrpcService<MeterReadingService>()
.EnableGrpcWeb()
.RequireCors("AllowAll");
// Reflection for Postman and other tools (dev only)
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}
// REST API coexisting with gRPC
app.MapControllers();
app.Run();
10. gRPC Clients — Multi-Language Guide
10.1 .NET Client (Worker Service)
// Worker.cs
public class MeterReaderWorker : BackgroundService
{
private readonly ILogger<MeterReaderWorker> _logger;
private readonly IConfiguration _configuration;
private MeterReaderService.MeterReaderServiceClient? _client;
private string _token = string.Empty;
private DateTime _tokenExpiration = DateTime.MinValue;
public MeterReaderWorker(ILogger<MeterReaderWorker> logger, IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var serviceUrl = _configuration["ServiceUrl"]!;
// Create the gRPC channel
var channel = GrpcChannel.ForAddress(serviceUrl);
_client = new MeterReaderService.MeterReaderServiceClient(channel);
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (NeedsLogin()) await RequestTokenAsync();
await SendReadingAsync(stoppingToken);
await Task.Delay(1000, stoppingToken);
}
catch (RpcException ex)
{
_logger.LogError(ex, "gRPC error: {StatusCode} - {Detail}",
ex.StatusCode, ex.Status.Detail);
await Task.Delay(5000, stoppingToken); // Backoff
}
}
}
private bool NeedsLogin() =>
string.IsNullOrEmpty(_token) || _tokenExpiration <= DateTime.UtcNow;
private async Task RequestTokenAsync()
{
var response = await _client!.GetTokenAsync(new TokenRequest
{
Username = _configuration["Auth:Username"]!,
Password = _configuration["Auth:Password"]!
});
if (response.Success)
{
_token = response.Token;
_tokenExpiration = DateTime.UtcNow.AddHours(1); // Refresh before expiration
_logger.LogInformation("Token acquired successfully");
}
}
private async Task SendReadingAsync(CancellationToken stoppingToken)
{
// Create metadata with the JWT token
var headers = new Metadata
{
{ "Authorization", $"Bearer {_token}" }
};
var packet = new ReadingPacket
{
Status = ReadingStatus.Success
};
packet.Readings.Add(new ReadingMessage
{
CustomerId = 1,
ReadingTime = Timestamp.FromDateTime(DateTime.UtcNow),
ReadingValue = Random.Shared.Next(100, 1000)
});
var result = await _client!.AddReadingAsync(packet,
headers: headers,
cancellationToken: stoppingToken);
_logger.LogInformation("Reading sent: {Success} - {Message}",
result.Success, result.Message);
}
}
10.2 gRPC Logging Interceptor
// Interceptors/LoggingInterceptor.cs
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
// Intercepts unary calls
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var methodName = context.Method;
var startTime = Stopwatch.StartNew();
_logger.LogInformation("gRPC call started: {Method}", methodName);
try
{
var response = await continuation(request, context);
startTime.Stop();
_logger.LogInformation(
"gRPC call completed: {Method} in {Elapsed}ms",
methodName, startTime.ElapsedMilliseconds);
return response;
}
catch (RpcException)
{
startTime.Stop();
_logger.LogWarning(
"gRPC call failed: {Method} after {Elapsed}ms",
methodName, startTime.ElapsedMilliseconds);
throw;
}
catch (Exception ex)
{
startTime.Stop();
_logger.LogError(ex,
"Unhandled exception in gRPC call {Method} after {Elapsed}ms",
methodName, startTime.ElapsedMilliseconds);
throw new RpcException(
new Status(StatusCode.Internal, ex.Message));
}
}
}
11. gRPC Streaming — Detailed Patterns
11.1 The 4 Streaming Types
graph LR
subgraph "1. Unary"
C1[Client] -->|"1 req"| S1[Server]
S1 -->|"1 resp"| C1
end
subgraph "2. Server Streaming"
C2[Client] -->|"1 req"| S2[Server]
S2 -->|"stream...resp"| C2
end
subgraph "3. Client Streaming"
C3[Client] -->|"stream...req"| S3[Server]
S3 -->|"1 resp"| C3
end
subgraph "4. Bidirectional"
C4[Client] <-->|"stream ↔ stream"| S4[Server]
end
11.2 Streaming Reading Example from the Client
// Client — Server-side streaming
public async Task ReadAllReadingsAsync(CancellationToken cancellationToken)
{
var headers = new Metadata { { "Authorization", $"Bearer {_token}" } };
using var call = _client!.GetReadings(
new Empty(),
headers: headers,
cancellationToken: cancellationToken);
_logger.LogInformation("Starting to receive readings stream");
await foreach (var reading in call.ResponseStream.ReadAllAsync(cancellationToken))
{
_logger.LogDebug(
"Received reading: Customer={CustomerId}, Value={Value}",
reading.CustomerId, reading.ReadingValue);
// Process each reading as it arrives
await ProcessReadingAsync(reading);
}
_logger.LogInformation("Readings stream completed");
}
// Client — Bidirectional streaming
public async Task ProcessReadingsStreamAsync(
IEnumerable<ReadingPacket> packets,
CancellationToken cancellationToken)
{
var headers = new Metadata { { "Authorization", $"Bearer {_token}" } };
using var call = _client!.ProcessReadings(headers: headers, cancellationToken: cancellationToken);
// Send packets and receive errors in parallel
var sendTask = SendPacketsAsync(call.RequestStream, packets, cancellationToken);
var receiveTask = ReceiveErrorsAsync(call.ResponseStream, cancellationToken);
await Task.WhenAll(sendTask, receiveTask);
}
private async Task SendPacketsAsync(
IClientStreamWriter<ReadingPacket> requestStream,
IEnumerable<ReadingPacket> packets,
CancellationToken cancellationToken)
{
foreach (var packet in packets)
{
await requestStream.WriteAsync(packet, cancellationToken);
await Task.Delay(10, cancellationToken); // Spacing
}
await requestStream.CompleteAsync();
}
private async Task ReceiveErrorsAsync(
IAsyncStreamReader<ErrorMessage> responseStream,
CancellationToken cancellationToken)
{
await foreach (var error in responseStream.ReadAllAsync(cancellationToken))
{
_logger.LogWarning("Error from server: [{Severity}] {Message}",
error.Severity, error.Message);
}
}
12. gRPC Best Practices
12.1 Proto Best Practices
// ✅ GOOD: Reserve deleted tags to avoid incompatibilities
message GoodMessage {
int32 field_one = 1;
// field_two was removed
int32 field_three = 3;
reserved 2; // Reserve tag 2 (cannot be reused)
reserved "field_two"; // Reserve the name
}
// ✅ GOOD: Use package names to avoid conflicts
package meterreader.v1; // Version the package!
option csharp_namespace = "MeterReader.V1.gRpc";
// ✅ GOOD: Protobuf naming conventions
// Messages: PascalCase
// Fields: snake_case (automatically converted to PascalCase in C#)
// Enums: ALL_CAPS_WITH_UNDERSCORES
// Service: PascalCase
// Methods: PascalCase
// ❌ BAD: Changing tag numbers
// message Bad {
// string name = 2; // Was 1, now 2 — incompatible!
// }
12.2 Best Practices Table
| Category | Best Practice | Explanation |
|---|---|---|
| Versioning | Version .proto packages | package meterreader.v2 |
| Tags | Never reuse a deleted tag | Use reserved |
| Defaults | Provide sensible default values | Enum with explicit 0 value |
| Errors | Use RpcException with StatusCode | StatusCode.NotFound, StatusCode.Internal |
| Security | [Authorize] on sensitive methods | JWT or client certificates |
| Performance | Use streaming for large volumes | Avoid packets > 4 MB |
| Cancellation | Respect context.CancellationToken | Check cancellation in loops |
| Logging | Use global interceptors | Avoid logging in each method |
| TLS | Always HTTPS in production | HTTP/2 requires TLS in production |
| gRPC-Web | Enable for JavaScript clients | UseGrpcWeb() middleware |
13. gRPC Glossary
| Term | Definition |
|---|---|
| Bidirectional Streaming | RPC where client and server simultaneously send streams of messages |
| Channel | Connection to a gRPC server (HTTP/2, multiplexed) |
| Client Streaming | RPC where the client sends multiple messages and receives one response |
| gRPC | Google Remote Procedure Call — high-performance RPC framework |
| gRPC-Web | gRPC extension for web browsers (gRPC→HTTP/1.1 proxy) |
| HTTP/2 | Protocol required by gRPC (multiplexing, header compression, binary) |
| Interceptor | gRPC middleware for logging, auth, retry at the call level |
| Metadata | Equivalent of HTTP headers in gRPC (key/value) |
| Protobuf (Protocol Buffers) | Google’s IDL language and binary serialization format |
| Proto3 | Version 3 of Protocol Buffers — all fields are optional |
| RpcException | gRPC exception with StatusCode and message |
| Server Streaming | RPC where the client sends one message and receives a stream of responses |
| ServerCallContext | Context of a gRPC call on the server side (metadata, cancellation, peer) |
| StatusCode | gRPC return code (OK, NotFound, Internal, Unauthenticated…) |
| Stub | gRPC client automatically generated from the .proto file |
| Tag | Unique number identifying a field in a Protobuf message |
| Timestamp | Protobuf type for dates/times (google.protobuf.Timestamp) |
| Unary RPC | Classic gRPC call: 1 request → 1 response |
Additional Resources
- Official gRPC Documentation
- gRPC for ASP.NET Core
- Protocol Buffers v3
- gRPC-Web for .NET
- gRPC Performance
- gRPC and Kubernetes
- Postman gRPC support
Search Terms
asp.net · grpc · core · web · apis · c# · .net · development · client · service · streaming · types · proto · protocol · authentication · buffer · clients · configuration · worker