Intermediate

Using gRPC in ASP.NET Core 10

Add gRPC to ASP.NET Core — protocol buffers, clients, streaming, security and best practices.

Demo application: MeterReader (IoT meter-reading service) ASP.NET Core / .NET 10, Visual Studio


Table of Contents

  1. What is gRPC?
  2. Adding gRPC to ASP.NET Core
  3. gRPC Clients
  4. Securing gRPC

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 .proto file

REST vs gRPC Comparison:

CriterionRESTgRPC
FormatJSON (text)Protobuf (binary)
TransportHTTP/1.1 or 2HTTP/2 required
ContractOptional (OpenAPI)Required (.proto)
StreamingSSE or WebSocketNative (4 types)
BrowserYesPartial (gRPC-Web)
PerformanceModerateVery high
ReadabilityHuman-readableNot 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 TypePython Type
int32intint
int64longint
floatfloatfloat
doubledoublefloat
boolboolbool
stringstringstr
bytesByteStringbytes
repeated TRepeatedField<T>list
map<K, V>MapField<K, V>dict
google.protobuf.TimestampTimestampTimestamp
google.protobuf.DurationDurationDuration

gRPC Streaming Types

TypeProtoClientServerUsage
Unaryrpc Method(Req) returns (Resp)One messageOne messageStandard requests
Server streamingrpc Method(Req) returns (stream Resp)One messageMultiple messagesHistory, events
Client streamingrpc Method(stream Req) returns (Resp)Multiple messagesOne messageBulk upload
Bidirectionalrpc Method(stream Req) returns (stream Resp)Multiple messagesMultiple messagesChat, 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
CriterionREST/JSONgRPCGraphQLSignalR
FormatJSON (text)Protobuf (binary)JSONJSON/binary
PerformanceAverageVery highAverageHigh
ContractsOpenAPI/SwaggerRequired (.proto)SchemaN/A
StreamingNot nativeYes (bidirectional)SubscriptionsYes
Browser support✅ Native⚠️ Via gRPC-Web✅ Native✅ Native
LanguagesAllAllAll.NET/JS
Readability✅ JSON❌ Binary✅ JSON✅ JSON
DebuggingEasy (Postman)Medium (Postman/grpcurl)EasyDifficult

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 TypeC# TypeExample
doubledouble3.14
floatfloat3.14f
int32int42
int64long9999999999
uint32uint42u
uint64ulong9999999999ul
boolbooltrue
stringstring"hello"
bytesByteStringbinary
google.protobuf.TimestampTimestampDateTimeTimestamp.FromDateTime(DateTime.UtcNow)
google.protobuf.DurationDurationduration
google.protobuf.EmptyEmptyempty message
repeated TRepeatedField<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

CategoryBest PracticeExplanation
VersioningVersion .proto packagespackage meterreader.v2
TagsNever reuse a deleted tagUse reserved
DefaultsProvide sensible default valuesEnum with explicit 0 value
ErrorsUse RpcException with StatusCodeStatusCode.NotFound, StatusCode.Internal
Security[Authorize] on sensitive methodsJWT or client certificates
PerformanceUse streaming for large volumesAvoid packets > 4 MB
CancellationRespect context.CancellationTokenCheck cancellation in loops
LoggingUse global interceptorsAvoid logging in each method
TLSAlways HTTPS in productionHTTP/2 requires TLS in production
gRPC-WebEnable for JavaScript clientsUseGrpcWeb() middleware

13. gRPC Glossary

TermDefinition
Bidirectional StreamingRPC where client and server simultaneously send streams of messages
ChannelConnection to a gRPC server (HTTP/2, multiplexed)
Client StreamingRPC where the client sends multiple messages and receives one response
gRPCGoogle Remote Procedure Call — high-performance RPC framework
gRPC-WebgRPC extension for web browsers (gRPC→HTTP/1.1 proxy)
HTTP/2Protocol required by gRPC (multiplexing, header compression, binary)
InterceptorgRPC middleware for logging, auth, retry at the call level
MetadataEquivalent of HTTP headers in gRPC (key/value)
Protobuf (Protocol Buffers)Google’s IDL language and binary serialization format
Proto3Version 3 of Protocol Buffers — all fields are optional
RpcExceptiongRPC exception with StatusCode and message
Server StreamingRPC where the client sends one message and receives a stream of responses
ServerCallContextContext of a gRPC call on the server side (metadata, cancellation, peer)
StatusCodegRPC return code (OK, NotFound, Internal, Unauthenticated…)
StubgRPC client automatically generated from the .proto file
TagUnique number identifying a field in a Protobuf message
TimestampProtobuf type for dates/times (google.protobuf.Timestamp)
Unary RPCClassic gRPC call: 1 request → 1 response

Additional Resources


Search Terms

asp.net · grpc · core · web · apis · c# · .net · development · client · service · streaming · types · proto · protocol · authentication · buffer · clients · configuration · worker

Interested in this course?

Contact us to book it or get a custom training plan for your team.