Skip to main content

Overview

gRPC is used for synchronous communication when services need immediate responses. The primary example is the Basket.API service calling Discount.Grpc to retrieve discount information for products.

Architecture Flow

Protocol Buffers Definition

The contract is defined in discount.proto:
syntax = "proto3";

option csharp_namespace = "Discount.Grpc";

package discount;

// The discount service definition.
service DiscountProtoService {
	// Discount CRUD Operations
	rpc GetDiscount (GetDiscountRequest) returns (CouponModel);
	rpc CreateDiscount (CreateDiscountRequest) returns (CouponModel);
	rpc UpdateDiscount (UpdateDiscountRequest) returns (CouponModel);
	rpc DeleteDiscount (DeleteDiscountRequest) returns (DeleteDiscountResponse);
}

message GetDiscountRequest {
	string productName = 1;
}

message CouponModel {
	int32 id = 1;
	string productName = 2;
	string description = 3;
	int32 amount = 4;
}

message CreateDiscountRequest {
	CouponModel coupon = 1;
}

message UpdateDiscountRequest {
	CouponModel coupon = 1;
}

message DeleteDiscountRequest {
	string productName = 1;
}

message DeleteDiscountResponse {
	bool success = 1;
}

Server Implementation (Discount.Grpc)

Service Registration

In Discount.Grpc/Program.cs:
using Discount.Grpc.Data;
using Discount.Grpc.Services;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddDbContext<DiscountContext>(opts =>
        opts.UseSqlite(builder.Configuration.GetConnectionString("Database")));

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseMigration();
app.MapGrpcService<DiscountService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.");

app.Run();

Service Implementation

In Discount.Grpc/Services/DiscountService.cs:
using Discount.Grpc.Data;
using Discount.Grpc.Models;
using Grpc.Core;
using Mapster;
using Microsoft.EntityFrameworkCore;

namespace Discount.Grpc.Services;

public class DiscountService
    (DiscountContext dbContext, ILogger<DiscountService> logger)
    : DiscountProtoService.DiscountProtoServiceBase
{    
    public override async Task<CouponModel> GetDiscount(GetDiscountRequest request, ServerCallContext context)
    {
        var coupon = await dbContext
            .Coupons
            .FirstOrDefaultAsync(x => x.ProductName == request.ProductName);

        if (coupon is null)
            coupon = new Coupon { ProductName = "No Discount", Amount = 0, Description = "No Discount Desc" };

        logger.LogInformation("Discount is retrieved for ProductName : {productName}, Amount : {amount}", 
            coupon.ProductName, coupon.Amount);

        var couponModel = coupon.Adapt<CouponModel>();
        return couponModel;
    }

    public override async Task<CouponModel> CreateDiscount(CreateDiscountRequest request, ServerCallContext context)
    {
        var coupon = request.Coupon.Adapt<Coupon>();
        if (coupon is null)
            throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid request object."));

        dbContext.Coupons.Add(coupon);
        await dbContext.SaveChangesAsync();

        logger.LogInformation("Discount is successfully created. ProductName : {ProductName}", coupon.ProductName);

        var couponModel = coupon.Adapt<CouponModel>();
        return couponModel;
    }

    // ... UpdateDiscount and DeleteDiscount methods
}

Client Implementation (Basket.API)

Client Registration

In Basket.API/Program.cs:
using Discount.Grpc;

var builder = WebApplication.CreateBuilder(args);

// Grpc Services
builder.Services.AddGrpcClient<DiscountProtoService.DiscountProtoServiceClient>(options =>
{
    options.Address = new Uri(builder.Configuration["GrpcSettings:DiscountUrl"]!);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback =
        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };

    return handler;
});

Configuration

In appsettings.json:
{
  "GrpcSettings": {
    "DiscountUrl": "https://localhost:5052"
  }
}

Client Usage

In Basket.API/Basket/StoreBasket/StoreBasketHandler.cs:
using Discount.Grpc;

namespace Basket.API.Basket.StoreBasket;

public class StoreBasketCommandHandler
    (IBasketRepository repository, DiscountProtoService.DiscountProtoServiceClient discountProto)
    : ICommandHandler<StoreBasketCommand, StoreBasketResult>
{
    public async Task<StoreBasketResult> Handle(StoreBasketCommand command, CancellationToken cancellationToken)
    {
        await DeductDiscount(command.Cart, cancellationToken);
        
        await repository.StoreBasket(command.Cart, cancellationToken);

        return new StoreBasketResult(command.Cart.UserName);
    }

    private async Task DeductDiscount(ShoppingCart cart, CancellationToken cancellationToken)
    {
        // Communicate with Discount.Grpc and calculate latest prices of products
        foreach (var item in cart.Items)
        {
            var coupon = await discountProto.GetDiscountAsync(
                new GetDiscountRequest { ProductName = item.ProductName }, 
                cancellationToken: cancellationToken);
            
            item.Price -= coupon.Amount;
        }
    }
}

Project References

In Basket.API.csproj, the proto file is referenced:
<ItemGroup>
  <Protobuf Include="..\..\Discount\Discount.Grpc\Protos\discount.proto" GrpcServices="Client">
    <Link>Protos\discount.proto</Link>
  </Protobuf>
</ItemGroup>
This generates:
  • Client stub classes
  • Message types
  • Serialization code

Key Benefits

gRPC uses HTTP/2 and Protocol Buffers for efficient binary serialization, making it significantly faster than REST/JSON.
The .proto contract ensures type safety across services. Compilation fails if contracts don’t match.
Both client and server code are generated from the .proto file, reducing boilerplate and potential errors.
gRPC includes deadlines, cancellation, authentication, and streaming out of the box.

Error Handling

The server throws RpcException for error cases:
if (coupon is null)
    throw new RpcException(new Status(StatusCode.NotFound, 
        $"Discount with ProductName={request.ProductName} is not found."));
The client can catch these exceptions:
try
{
    var coupon = await discountProto.GetDiscountAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    // Handle not found
}

Best Practices

In production, use proper SSL certificate validation instead of DangerousAcceptAnyServerCertificateValidator.
Consider implementing retry policies using Polly for transient failures:
builder.Services.AddGrpcClient<DiscountProtoService.DiscountProtoServiceClient>()
    .AddPolicyHandler(GetRetryPolicy());

Next Steps

RabbitMQ Messaging

Learn about asynchronous event-based communication

MassTransit Setup

Configure MassTransit for the message broker

Build docs developers (and LLMs) love