Implementing gRPC in ASP.NET Core

Implementing gRPC in ASP.NET Core Microservices

In a microservices-based architecture, services often need to communicate with each other quickly, reliably, and at scale. While REST APIs are widely used for client-facing communication, they may introduce unnecessary overhead when used for internal, high-frequency service-to-service calls. gRPC is a modern, high-performance communication framework built on HTTP/2 and Protocol Buffers, designed specifically to address these challenges.

Now, we will explore how gRPC can be practically implemented in a real-world E-Commerce microservices system to improve performance, enforce strong contracts, and maintain clean architectural boundaries, all while coexisting seamlessly with existing REST APIs.

Real-Time Scenario: Fetching User and Product Details using gRPC

In an E-Commerce application, Order Creation is a critical operation. Before an order can be successfully created, the system must validate multiple pieces of information in real time. These validations must happen synchronously, meaning the OrderService must wait for the response before moving to the next step.

In our system, the OrderService does not own user data or product data. Instead, it communicates with other microservices to fetch and validate this information. This is where gRPC becomes extremely useful.

User Validation using UserService

Before creating an order, the OrderService must first confirm that the user placing the order is valid. This step ensures that the system does not create orders for invalid or unauthorized users. The OrderService communicates with the UserService to check:

  • Whether the User exists
  • Whether the User is active
  • Whether the User is verified
  • Whether the User is allowed to place orders
  • Fetch basic user information, such as Name, Email, and Phone Number, for order records and communication

This interaction follows a simple request–response pattern:

  • The OrderService sends a UserId
  • The UserService returns user details or indicates that the user does not exist

Since this is a lightweight, synchronous operation with a clear request and response, it is an ideal use case for a gRPC Unary RPC.

Fetching Product Details using ProductService

After validating the user, the OrderService must validate and fetch product-related information. This step is essential to ensure that the order is accurate and can be fulfilled. The OrderService requests the following information from the ProductService:

  • Product Name
  • Current Price
  • Discounted Price (if applicable)
  • Stock Availability
  • Product Status

These product-related queries are:

  • Very frequent
  • Time-sensitive
  • Performance critical

Even a small delay in fetching this data can significantly increase the overall time to place an order. Because these requests happen thousands of times per minute, using traditional REST APIs with JSON introduces unnecessary overhead, such as:

  • Larger payload sizes
  • Higher CPU usage for serialization and deserialization
  • Increased network latency
  • Slower overall response times

Why gRPC is a Better Choice Here

gRPC uses:

  • Protocol Buffers (binary format) instead of JSON
  • HTTP/2, which supports multiplexing and efficient network usage
  • Strongly typed contracts, generated at compile time

Because of these advantages, gRPC provides faster, more efficient, and more reliable internal communication between microservices. This makes it the perfect choice for internal, high-frequency service-to-service calls, such as those required during order creation.

Order Creation Flow using gRPC-based Validation

During order placement, the OrderService performs multiple gRPC calls to validate and prepare the order.

UserService gRPC Calls

Call 1: Get User by ID
  • Method: GetUserByIdAsync(request.UserId, accessToken)
  • Input:
      • UserId (Guid)
      • Access Token (for authentication)
  • Output:
      • UserDTO containing Id, FullName, Email, and PhoneNumber
  • Purpose:
      • Ensure the user exists
      • Fail immediately if the user is invalid

If the user is not found, the order creation process is stopped immediately.

Call 2: Save or Update Shipping Address
  • Method: SaveOrUpdateAddressAsync(request.ShippingAddress, accessToken)
  • When Called:
      • Only if ShippingAddressId is null
      • And a new shipping address is provided
  • Output:
      • Newly created or updated AddressId
  • Purpose:
      • Ensure the shipping address exists before order creation
Call 3: Save or Update Billing Address
  • Method: SaveOrUpdateAddressAsync(request.BillingAddress, accessToken)
  • Logic:
      • Same as shipping address handling
  • Purpose:
      • Ensure billing details are properly stored

ProductService gRPC Calls

Call 1: Check Product Availability

  • Method: CheckProductsAvailabilityAsync(stockCheckRequests, accessToken)
  • Input:
      • List of products with requested quantities
  • Output:
      • Availability status for each product
  • Purpose:
      • Fail fast if:
          • Product is invalid
          • The requested quantity is not available

This step prevents the creation of orders that cannot be fulfilled.

Call 2: Get Product Details by IDs
  • Method: GetProductsByIdsAsync(productIds, accessToken)
  • Input:
      • List of ProductIds
  • Output:
      • Product details including:
          • Name
          • Price
          • Discounted Price
          • Stock Quantity
  • Purpose:
      • Ensure accurate pricing and discount calculation at the time of order creation

Why This Scenario Is Ideal for gRPC

This order creation flow perfectly demonstrates where gRPC should be used:

  • High-frequency calls
  • Synchronous service-to-service communication
  • Internal microservices (not exposed to public clients)
  • Strong performance requirements
  • Small, structured request–response messages

This is exactly the type of scenario where gRPC outperforms REST, and why it is commonly used in real-world, large-scale microservices systems.

Step-1: Create a Shared gRPC Contracts Project

In a microservices architecture, different services must agree on how they communicate. When we use gRPC, this agreement is written using Protocol Buffer (.proto) files. These files define:

  • What services exist
  • What methods do they expose
  • What data is sent and received

Because these definitions are shared between the gRPC client and the gRPC server, we create one common project that contains only the gRPC contracts. This shared project is then referenced by all microservices that either:

  • Host a gRPC service, or
  • Call a gRPC service

This avoids duplication, inconsistencies, and version mismatch problems.

Step 1: Create a Solution Folder

Inside your main solution, create a solution folder named: GRPC

This folder is used only to organize gRPC-related projects and keep them separate from application logic.

Step 2: Create the Contracts Project

Inside the GRPC solution folder, add a new Class Library project named ECommerce.GrpcContracts

This project will:

  • Contain only .proto files
  • Does not contain any business logic
  • Not hosting any gRPC services
  • Be shared across multiple microservices

Step 3: Installing Required NuGet Packages

Inside the ECommerce.GrpcContracts project, install the following NuGet packages using the Package Manager Console:

  • Install-Package Google.Protobuf
  • Install-Package Grpc.Tools
Why These Packages Are Required
  • Google.Protobuf: This package provides the core Protocol Buffers serialization support. It understands how to convert binary data into objects and vice versa.
  • Grpc.Tools: This package is responsible for generating C# code from .proto files at build time. It generates:
      • Message classes
      • Service-base classes
      • gRPC client stubs

Think of Grpc.Tools as a code generator and Google.Protobuf is the engine that runs the generated code.

Step 4: Creating the Folder Structure

Inside the ECommerce.GrpcContracts project, create a folder named: Protos

This folder will store all .proto files. Keeping them in one place improves readability and makes versioning easier.

What Is a .proto File?

A .proto file is a plain text file with a .proto extension. It is used to define:

  • gRPC services
  • Request and response message formats
  • Data types exchanged between services

You can think of a .proto file as a contract that both the client and the server must follow.

Creating user.proto

Steps to Create the File

  1. Right-click on the Protos folder
  2. Select Add → New Item
  3. In the search box, type Text
  4. Select Text File
  5. Set the file name to: user.proto
  6. Click Add as shown in the image below.

Implementing gRPC in ASP.NET Core Microservices

You will now see an empty user.proto file open in the editor. Then please copy-paste the following code into it. The following code is self-explained, so please read the comment lines for a better understanding.

// -------------------------------
// Protocol Buffers version
// -------------------------------
// - proto3 is the latest and recommended syntax.
// - Default values are used when fields are missing
syntax = "proto3";

// -------------------------------
// C# namespace for generated code
// -------------------------------
// Controls the C# namespace of the generated classes.
// Example generated types will be:
// - ECommerce.GrpcContracts.Users.UserGrpc
// - ECommerce.GrpcContracts.Users.UserExistsRequest, etc.
option csharp_namespace = "ECommerce.GrpcContracts.Users";

// -------------------------------
// Protobuf package name
// -------------------------------
// This is NOT a C# namespace.
// It is used internally by protobuf for namespacing
// and to avoid conflicts across services/languages.
package ecommerce.users;

// -------------------------------
// gRPC Service Definition
// -------------------------------
// A gRPC service is similar to a Controller in REST,
// but instead of URLs, it exposes METHOD calls.
// This defines a gRPC service named "UserGrpc".
// Each RPC method inside maps to a callable endpoint.
service UserGrpc {
  // ----------------------------------
  // RPC: Checks whether a user exists
  // ----------------------------------
  // Used when OrderService only needs to validate
  // whether a UserId is valid.
  // Input  : UserExistsRequest (contains user_id)
  // Output : UserExistsReply   (contains exists = true/false)
  rpc UserExists (UserExistsRequest) returns (UserExistsReply);

  // ----------------------------------
  // RPC: Fetch user profile by UserId
  // ----------------------------------
  // Returns basic user info required during order creation
  // (Name, Email, Phone, etc.)
  // Input  : GetUserByIdRequest
  // Output : GetUserByIdReply (found flag + user object)
  rpc GetUserById (GetUserByIdRequest) returns (GetUserByIdReply);

  // -----------------------------------------
  //  RPC: Fetch a specific address of a user
  // -----------------------------------------
  // Used when OrderService needs shipping/billing address by user id + address id.
  // Input  : GetUserAddressByIdRequest
  // Output : GetUserAddressByIdReply (found flag + address object)
  rpc GetUserAddressById (GetUserAddressByIdRequest) returns (GetUserAddressByIdReply);

  // -------------------------------
  // RPC: Create or update an address
  // -------------------------------
  // If Address.Id is empty => create new
  // If Address.Id is present => update existing
  // Output returns the saved address_id
  rpc SaveOrUpdateAddress (SaveOrUpdateAddressRequest) returns (SaveOrUpdateAddressReply);
}

// -------------------------------
// Request / Response Models
// -------------------------------

// Request message for UserExists RPC.
message UserExistsRequest {
 // Uses string instead of Guid because:
 // - Protobuf has no Guid type
 // - String is language-agnostic

 // field_number (1) is used in the binary format.
 // Never change existing numbers (breaking change).

 // User ID as string (Guid passed as string)
 string user_id = 1;
}

// Response message for UserExists RPC.
message UserExistsReply {
  // true if user exists, false otherwise
  bool exists = 1;
}

// Request message for GetUserById RPC.
message GetUserByIdRequest {
 // The user ID to search for.  
 // user Guid as string
 string user_id = 1;
}

// Response for GetUserById RPC.
// "found"   → whether user exists.
// "user"    → the user details (only if found == true)
message GetUserByIdReply {
 // true if user found, false if not found
 bool found = 1;

 // If found == true, this should contain user details.
 // If found == false, this may be empty / default.
 User user = 2;
}

// -------------------------------
// USER MESSAGE MODEL
// -------------------------------
// Represents the user details returned to OrderService.
// This is the Protobuf representation of a User.
// Equivalent to UserDTO in your application layer.
message User {
  string id = 1;            // UserId (GUID as string)
  string full_name = 2;     // Full name of the user
  string email = 3;         // Email address
  string phone_number = 4;  // Phone number
}

// -----------------------------
// GET USER ADDRESS — REQUEST
// -----------------------------
// Request for fetching user address by user_id and address_id.
// OrderService passes both:
// user_id     → who owns the address
// address_id  → which address to retrieve
message GetUserAddressByIdRequest {
 string user_id = 1;      // UserId (Guid as string)
 string address_id = 2;   // AddressId (Guid as string)
}

// -----------------------------
// GET USER ADDRESS — RESPONSE
// -----------------------------
// Response for GetUserAddressById.
// found    → whether address exists
// address  → address object (if found)
message GetUserAddressByIdReply {
  // true if address found for that user_id + address_id
  bool found = 1;

  // If found == true, this contains the address details.
  Address address = 2;
}

// ----------------------------------
// SAVE/UPDATE ADDRESS — REQUEST
// ----------------------------------
// Request for SaveOrUpdateAddress RPC.
// Contains a nested Address message.
// If address.id is empty → create new
// If address.id is set   → update existing
message SaveOrUpdateAddressRequest {
   // Address object containing all fields
  Address address = 1;
}

// ---------------------------------
// SAVE/UPDATE ADDRESS — RESPONSE
// ---------------------------------
// Response for SaveOrUpdateAddress RPC.
message SaveOrUpdateAddressReply {
  // The saved/updated address ID (Guid as string)
  // Useful because when creating a new address,
  // OrderService needs the generated address_id to store in Order.
  string address_id = 1;
}

// ---------------------------------
// ADDRESS MESSAGE MODEL
// ---------------------------------
// Represents shipping/billing address
message Address {
  // Address ID (Guid as string)
  // Convention used here:
  // - empty string => create new address
  // - non-empty    => update existing address
  string id = 1;

  // User ID to which this address belongs (Guid as string)
  string user_id = 2;

  // Address line 1 (house/building/street)
  string address_line1 = 3;

  // Address line 2 (landmark/apartment/etc.)
  // Can be empty string if not used
  string address_line2 = 4;

  // City
  string city = 5;

  // State
  string state = 6;

  // Postal / ZIP code
  string postal_code = 7;

  // Country
  string country = 8;

  // Whether this is the default billing address for the user
  bool is_default_billing = 9;

  // Whether this is the default shipping address for the user
  bool is_default_shipping = 10;
}

Create product.proto

It defines:

  • Product-related gRPC services
  • Messages for stock validation
  • Bulk operations for performance

Please follow the previous steps, create product.proto in the Protos folder, and copy-paste the following code.

// PROTO3 syntax
// We are using Protocol Buffers v3 syntax.
syntax = "proto3";

// Controls the C# namespace for the generated C# classes.
// Example generated types:
// - ECommerce.GrpcContracts.Products.ProductGrpc
// - ECommerce.GrpcContracts.Products.Product, GetProductsByIdsRequest, etc.
option csharp_namespace = "ECommerce.GrpcContracts.Products";

// Protobuf "package" to logically group messages/services in protobuf world.
// Helps avoid name collisions across multiple .proto files.
package ecommerce.products;

// gRPC service contract exposed by ProductService.
service ProductGrpc {

  // RPC: Fetch product details for multiple product IDs in one call.
  // Used by OrderService during order creation for pricing/discounts/stock info.
  // Input  : list of product IDs
  // Output : list of product objects
  rpc GetProductsByIds (GetProductsByIdsRequest) returns (GetProductsByIdsReply);

  // RPC: Validate each ordered product:
  // - does the product exist?
  // - is requested quantity available?
  // Used by OrderService before creating an order (fail-fast).
  rpc CheckProductsAvailability (CheckProductsAvailabilityRequest) returns (CheckProductsAvailabilityReply);

  // RPC: Increase stock for multiple products at once.
  // Used in scenarios like:
  // - order cancellation
  // - return approved
  // - compensation in saga pattern
  rpc IncreaseStockBulk (StockBulkUpdateRequest) returns (StockBulkUpdateReply);

  // RPC: Decrease stock for multiple products at once.
  // Used in scenarios like:
  // - order placement / confirmation
  // - reservation confirmed
  rpc DecreaseStockBulk (StockBulkUpdateRequest) returns (StockBulkUpdateReply);
}

// Request for GetProductsByIds.
// A list of product IDs to fetch.
// "repeated string" means:
// - A list/array of strings
// - Each string is a ProductId (Guid)
message GetProductsByIdsRequest {
  repeated string product_ids = 1; // A list of product IDs to fetch
}

// Response for GetProductsByIds.
// Returns multiple Product messages.
message GetProductsByIdsReply {
  repeated Product products = 1; // List of products returned
}

// Product object returned by ProductService.
// Keep it minimal: only what OrderService needs during order creation.
message Product {
  // Product ID (Guid as string)
  string id = 1;

  // Product name
  string name = 2;

  // Price fields are strings to avoid floating-point precision issues.
  // In C#, your service will convert:
  // decimal -> string using ToString(CultureInfo.InvariantCulture)
  // and in client convert back:
  // string -> decimal using decimal.Parse(..., CultureInfo.InvariantCulture)
  string price = 3;
  string discounted_price = 4;

  // Current available stock quantity
  int32 stock_quantity = 5;
}

// One stock check request item: product + requested quantity.
message StockCheckItem {
  // Product ID (Guid as string)
  string product_id = 1;

  // Quantity requested by OrderService
  int32 quantity = 2;
}

// Result of validating one product for stock availability.
message StockCheckResult {
  // Product ID for which this result applies
  string product_id = 1;

  // true => product exists and is valid
  // false => product does not exist / inactive / invalid product id
  bool is_valid_product = 2;

  // true => requested quantity is available
  // false => stock is insufficient
  bool is_quantity_available = 3;

  // Stock available at time of validation
  // (useful for UI messages or decision making)
  int32 available_quantity = 4;
}

// Request to verify multiple products' stock
// Stock check request → list of StockCheckItem
// Uses bulk verification for performance.
message CheckProductsAvailabilityRequest {
  repeated StockCheckItem items = 1;
}

// Stock check reply → list of StockCheckResult
message CheckProductsAvailabilityReply {
  repeated StockCheckResult results = 1;
}

// Used for both increase and decrease operations.
message StockUpdate {
  // Product ID (Guid as string)
  string product_id = 1;

  // Quantity to adjust (increase or decrease depending on RPC)
  int32 quantity = 2;
}

// Request for bulk stock updates.
// Contains many StockUpdate items.
// Allows multiple stock updates in a single call.
message StockBulkUpdateRequest {
  repeated StockUpdate updates = 1;
}

// Response after bulk stock update is applied.
// success = true  => operation completed
// success = false => operation failed, message describes why
message StockBulkUpdateReply {
  bool success = 1;
  string message = 2;
}
Update ECommerce.GrpcContracts.csproj

Please update the project file as follows. GrpcServices=”None” → we generate client/server code in the microservices, NOT here.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.33.2" />
    <PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
    <PackageReference Include="Grpc.Tools" Version="2.76.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\user.proto" GrpcServices="None" />
    <Protobuf Include="Protos\product.proto" GrpcServices="None" />
  </ItemGroup>
</Project>

This tells the compiler:

  • Do not generate gRPC server or client code here
  • This project is contracts-only
  • Actual code generation will happen inside microservices

This keeps responsibilities clean and avoids accidental service hosting.

Step 2: Adding gRPC Server Support to Existing Microservices

In our E-Commerce system, we already have UserService and ProductService running as independent microservices with REST APIs. Since these services are already stable and in use, we do not create new microservices for gRPC. Instead, we enhance the existing services by adding gRPC endpoints alongside REST APIs. This approach has several benefits:

  • No breaking changes to existing REST clients
  • REST remains available for UI and external consumers
  • gRPC is used only for internal service-to-service communication
  • Clean, incremental adoption of gRPC

This is a real-world industry approach, not a theoretical setup.

Install Required gRPC Packages

Inside both:

  • UserService.API
  • ProductService.API

Install the following NuGet packages by executing the commands below in Package Manager Console:

  • Install-Package Grpc.AspNetCore
  • Install-Package Google.Protobuf
  • Install-Package Grpc.Tools
Why These Packages Are Needed
  • Grpc.AspNetCore: Enables hosting gRPC services inside an ASP.NET Core application.
  • Google.Protobuf: Handles serialization and deserialization of Protocol Buffer messages.
  • Grpc.Tools: Generates server-side C# classes from .proto files during build.

Think of this step as “teaching the microservice how to speak gRPC”.

Linking user.proto into UserService.API

The .proto files live in the shared contracts project, but UserService must generate server code from them. Instead of copying the file (which causes duplication), we link it.

Steps to Add user.proto
  1. Right-click UserService.API project
  2. Select Add → Existing Item
  3. Browse to: ECommerce.GrpcContracts\Protos\user.proto
  4. Click the dropdown next to Add
  5. Select Add As Link as shown in the image below:

Linking user.proto into UserService.API

This ensures:

  • Only one source of truth
  • No duplicated .proto files
  • Changes propagate automatically
Configure the Linked File

After adding the file:

  • Right-click user.proto → Properties
  • Set Build Action = Protobuf
  • Set GrpcServices = Server Only as shown in the image below:

Implementing gRPC in ASP.NET Core Web API Microservices

If the UI does not show this option, update the project file manually. Add the following to the project file:

<ItemGroup>
  <Protobuf Include="..\ECommerce.GrpcContracts\Protos\user.proto" GrpcServices="Server">
    <Link>user.proto</Link>
  </Protobuf>
</ItemGroup>
What This Configuration Means
  • GrpcServices=”Server” → Generate server-side base classes only
  • No client code is generated here
  • This service becomes the host of UserGrpc
Add product.proto Link Reference in ProductService.API.csproj

Repeat the Same Steps for ProductService. Follow the exact same process to add: product.proto to ProductService.API.

The only difference:

  • ProductService generates server stubs for ProductGrpc
  • UserService generates server stubs for UserGrpc

UserService gRPC Server Implementation

Inside UserService.API:

  1. Create a folder named GrpcServices
  2. Add a class UserGrpcService.cs and then copy-paste the following code.

This class implements the RPC methods defined in user.proto. The following code is self-explanatory, so please read the comment lines.

using Grpc.Core;                                  
using UserService.Application.Services;           
using ECommerce.GrpcContracts.Users;             

namespace UserService.API.GrpcServices
{
    // ============================================================================
    // gRPC Server Implementation for UserService
    // ----------------------------------------------------------------------------
    // This class IMPLEMENTS the RPC operations defined in user.proto:
    //
    // service UserGrpc {
    //   rpc UserExists(...)
    //   rpc GetUserById(...)
    //   rpc GetUserAddressById(...)
    //   rpc SaveOrUpdateAddress(...)
    // }
    //
    // The generated base class is: UserGrpc.UserGrpcBase
    // We extend it and provide the actual implementation using our IUserService.
    // ============================================================================

    public sealed class UserGrpcService : UserGrpc.UserGrpcBase
    {
        private readonly IUserService _userService;   // Application-layer UserService
        private readonly ILogger<UserGrpcService> _logger; // Logger to log messages

        // DI constructor — receives IUserService via dependency injection.
        public UserGrpcService(IUserService userService, ILogger<UserGrpcService> logger)
        {
            _userService = userService ?? throw new ArgumentNullException(nameof(userService));
            _logger = logger;
        }

        // ============================================================================
        // 1. USER EXISTS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Quickly check if a user exists.
        //   - Used by OrderService before order creation.
        //
        // Flow:
        //   (a) Validate incoming UserId (string → Guid)
        //   (b) Call application service IUserService.IsUserExistsAsync
        //   (c) Return a boolean reply
        // ============================================================================
        public override async Task<UserExistsReply> UserExists(UserExistsRequest request, ServerCallContext context)
        {
            _logger.LogInformation(
                "gRPC UserExists called. user_id={UserId}",
                request.UserId);

            // Validate user_id format — must be a GUID.
            if (!Guid.TryParse(request.UserId, out var userId))
            {
                _logger.LogWarning(
                    "Invalid user_id received in UserExists. value={UserId}",
                    request.UserId);

                throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid user_id."));
            }
              
            // Use existing application service to check existence.
            var exists = await _userService.IsUserExistsAsync(userId);

            _logger.LogInformation(
                "UserExists completed. user_id={UserId}, exists={Exists}",
                userId, exists);

            // Return a protobuf reply message.
            return new UserExistsReply { Exists = exists };
        }

        // ============================================================================
        // 2. GET USER BY ID
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch complete user profile
        //   - Used during order creation for delivery and payment info.
        //
        // Steps:
        //   (a) Validate GUID input
        //   (b) Call _userService.GetProfileAsync
        //   (c) Map application DTO → protobuf User message
        //   (d) Return reply
        // ============================================================================
        public override async Task<GetUserByIdReply> GetUserById(GetUserByIdRequest request, ServerCallContext context)
        {
            _logger.LogInformation(
                "gRPC GetUserById called. user_id={UserId}",
                request.UserId);

            // Validate user_id format.
            if (!Guid.TryParse(request.UserId, out var userId))
            {
                _logger.LogWarning(
                    "Invalid user_id received in GetUserById. value={UserId}",
                    request.UserId);

                throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid user_id."));
            }
             
            // Get user profile from existing application logic.
            var profile = await _userService.GetProfileAsync(userId);

            // If not found → return Found = false
            if (profile == null)
            {
                _logger.LogInformation(
                    "User not found. user_id={UserId}",
                    userId);

                return new GetUserByIdReply { Found = false };
            }

            _logger.LogInformation(
                "User profile retrieved successfully. user_id={UserId}",
                userId);

            // Map UserService.Application.DTO → Protobuf User message
            return new GetUserByIdReply
            {
                Found = true,
                User = new ECommerce.GrpcContracts.Users.User
                {
                    Id = profile.UserId.ToString(),
                    FullName = profile.FullName ?? string.Empty,
                    Email = profile.Email ?? string.Empty,
                    PhoneNumber = profile.PhoneNumber ?? string.Empty
                }
            };
        }

        // ============================================================================
        // 3. GET USER ADDRESS BY ID
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch a specific address belonging to a user
        //   - Used during order creation to validate shipping/billing addresses
        //
        // Steps:
        //   (a) Validate both GUIDs (userId + addressId)
        //   (b) Call _userService.GetAddressByUserIdAndAddressIdAsync
        //   (c) Map application-layer AddressDTO → protobuf Address message
        // ============================================================================
        public override async Task<GetUserAddressByIdReply> GetUserAddressById(GetUserAddressByIdRequest request, ServerCallContext context)
        {
            _logger.LogInformation(
               "gRPC GetUserAddressById called. user_id={UserId}, address_id={AddressId}",
               request.UserId, request.AddressId);

            // Validate user ID
            if (!Guid.TryParse(request.UserId, out var userId))
            {
                _logger.LogWarning(
                    "Invalid user_id in GetUserAddressById. value={UserId}",
                    request.UserId);

                throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid user_id."));
            }

            // Validate address ID
            if (!Guid.TryParse(request.AddressId, out var addressId))
            {
                _logger.LogWarning(
                    "Invalid address_id in GetUserAddressById. value={AddressId}",
                    request.AddressId);

                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "Invalid address_id."));
            }

            // Get address via existing domain/application rules.
            var address = await _userService.GetAddressByUserIdAndAddressIdAsync(userId, addressId);

            // If address does not exist → Found = false
            if (address == null)
            {
                _logger.LogInformation(
                    "Address not found. user_id={UserId}, address_id={AddressId}",
                    userId, addressId);

                return new GetUserAddressByIdReply { Found = false };
            }

            _logger.LogInformation(
               "Address retrieved successfully. user_id={UserId}, address_id={AddressId}",
               userId, addressId);

            // Map Domain AddressDTO → Protobuf Address message.
            return new GetUserAddressByIdReply
            {
                Found = true,
                Address = new ECommerce.GrpcContracts.Users.Address
                {
                    Id = address.Id?.ToString() ?? string.Empty,
                    UserId = address.userId.ToString(),
                    AddressLine1 = address.AddressLine1,
                    AddressLine2 = address.AddressLine2 ?? string.Empty,
                    City = address.City,
                    State = address.State,
                    PostalCode = address.PostalCode,
                    Country = address.Country,
                    IsDefaultBilling = address.IsDefaultBilling,
                    IsDefaultShipping = address.IsDefaultShipping
                }
            };
        }

        // ============================================================================
        // 4. SAVE OR UPDATE ADDRESS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Add a new address OR update an existing one.
        //   - Part of user profile management.
        //
        // Steps:
        //   (a) Validate address is not null
        //   (b) Validate userId and (optional) addressId
        //   (c) Convert protobuf Address → AddressDTO (application model)
        //   (d) Use _userService.AddOrUpdateAddressAsync
        //   (e) Return the updated/new addressId
        // ============================================================================
        public override async Task<SaveOrUpdateAddressReply> SaveOrUpdateAddress(SaveOrUpdateAddressRequest request, ServerCallContext context)
        {
            _logger.LogInformation("gRPC SaveOrUpdateAddress called.");

            // Address must be provided.
            if (request.Address == null)
            {
                _logger.LogWarning("SaveOrUpdateAddress failed. Address is null.");

                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "Address is required."));
            }

            var a = request.Address;

            // Validate and convert UserId
            if (!Guid.TryParse(a.UserId, out var userId))
            {
                _logger.LogWarning(
                    "Invalid user_id in SaveOrUpdateAddress. value={UserId}",
                    a.UserId);

                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "Invalid user_id."));
            }

            // Optional AddressId (only if updating)
            Guid? addressId = null;

            if (!string.IsNullOrWhiteSpace(a.Id))
            {
                if (!Guid.TryParse(a.Id, out var parsedId))
                {
                    _logger.LogWarning(
                        "Invalid address_id in SaveOrUpdateAddress. value={AddressId}",
                        a.Id);

                    throw new RpcException(
                        new Status(StatusCode.InvalidArgument, "Invalid address_id."));
                }

                addressId = parsedId;
            }

            // Convert protobuf Address → Application DTO (AddressDTO)
            var dto = new UserService.Application.DTOs.AddressDTO
            {
                Id = addressId,
                userId = userId,
                AddressLine1 = a.AddressLine1,
                AddressLine2 = string.IsNullOrWhiteSpace(a.AddressLine2) ? null : a.AddressLine2,
                City = a.City,
                State = a.State,
                PostalCode = a.PostalCode,
                Country = a.Country,
                IsDefaultBilling = a.IsDefaultBilling,
                IsDefaultShipping = a.IsDefaultShipping
            };

            // Save or update using the application-layer method.
            var savedId = await _userService.AddOrUpdateAddressAsync(dto);

            _logger.LogInformation(
                "Address saved successfully. user_id={UserId}, address_id={AddressId}",
                userId, savedId);

            // Return addressId to the caller (OrderService / UI service).
            return new SaveOrUpdateAddressReply { AddressId = savedId.ToString() };
        }
    }
}
What This Class Represents
  • It is the gRPC equivalent of a REST Controller
  • It does not contain business logic
  • It delegates work to IUserService (Application Layer)
  • It performs:
      • Validation
      • Mapping
      • Logging
      • Error translation (to gRPC status codes)

This maintains Clean Architecture principles.

Wire in UserService.API/Program.cs

Please add the following service and middleware components.

Add Service

  • builder.Services.AddGrpc();

And Middleware:

  • app.MapGrpcService<UserService.API.GrpcServices.UserGrpcService>();
builder.Services.AddGrpc():

This line registers gRPC support in the ASP.NET Core DI container. Think of it as saying: This application is capable of hosting gRPC services. When we call AddGrpc():

  • gRPC server infrastructure is added
  • HTTP/2 pipeline support is enabled
  • Protobuf serialization/deserialization is registered
  • gRPC model binding is wired
  • gRPC-specific middleware services are registered
app.MapGrpcService<UserGrpcService>():

This is middleware endpoint mapping (routing registration). When we call: app.MapGrpcService<UserService.API.GrpcServices.UserGrpcService>(); We are telling ASP.NET Core: Expose the RPC methods defined in user.proto (UserGrpc service) at runtime.

Internally this:

  • Registers gRPC endpoints into the ASP.NET Core routing system
  • Connects incoming HTTP/2 requests to our service implementation
  • Uses the generated contract (from UserGrpc.UserGrpcBase) to know which RPC methods exist

ProductService gRPC Server Implementation

The ProductService implementation follows the same pattern. Key responsibilities of ProductGrpcService:

  • Bulk product fetching
  • Stock availability validation
  • Bulk stock updates (increase/decrease)
  • Performance-optimized service-to-service communication

Inside the Product.API project, create a folder named GrpcServices. Then, inside the GrpcServices folder, create a class file named ProductGrpcService.cs and copy and paste the following code. The following code is self-explanatory; please read the comment lines for better understanding.

using ECommerce.GrpcContracts.Products;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using ProductService.Application.DTOs;
using ProductService.Application.Interfaces;
using System.Globalization;

namespace ProductService.API.GrpcServices
{
    // ============================================================================
    // gRPC Server Implementation for ProductService
    // ----------------------------------------------------------------------------
    // This class implements the RPC operations defined in product.proto:
    //
    // service ProductGrpc {
    //   rpc GetProductsByIds(...)
    //   rpc CheckProductsAvailability(...)
    //   rpc IncreaseStockBulk(...)
    //   rpc DecreaseStockBulk(...)
    // }
    //
    // Notes:
    // - This is the gRPC equivalent of a REST Controller
    // - Business logic lives in Application layer (IProductService, IInventoryService)
    // - This class performs validation, mapping, logging, and error translation
    // ============================================================================
    public sealed class ProductGrpcService : ProductGrpc.ProductGrpcBase
    {
        private readonly IProductService _productService;
        private readonly IInventoryService _inventoryService;
        private readonly ILogger<ProductGrpcService> _logger;

        // Constructor with dependency injection
        public ProductGrpcService(
            IProductService productService,
            IInventoryService inventoryService,
            ILogger<ProductGrpcService> logger)
        {
            _productService = productService ?? throw new ArgumentNullException(nameof(productService));
            _inventoryService = inventoryService ?? throw new ArgumentNullException(nameof(inventoryService));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        // ============================================================================
        // 1. GET PRODUCTS BY IDS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch product details in bulk
        //   - Used during order creation to calculate pricing and discounts
        //
        // Flow:
        //   (a) Validate product_ids
        //   (b) Convert string IDs → Guid
        //   (c) Call application service
        //   (d) Map domain entities → protobuf Product messages
        // ============================================================================
        public override async Task<GetProductsByIdsReply> GetProductsByIds(
            GetProductsByIdsRequest request,
            ServerCallContext context)
        {
            _logger.LogInformation(
                "gRPC GetProductsByIds called. count={Count}",
                request.ProductIds?.Count ?? 0);

            // Validate request
            if (request.ProductIds == null || request.ProductIds.Count == 0)
            {
                _logger.LogWarning("GetProductsByIds failed: product_ids missing.");
                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "product_ids required."));
            }

            // Convert product IDs from string → Guid
            var ids = request.ProductIds.Select(id =>
            {
                if (!Guid.TryParse(id, out var gid))
                {
                    _logger.LogWarning("Invalid product_id received: {ProductId}", id);
                    throw new RpcException(
                        new Status(StatusCode.InvalidArgument, $"Invalid product_id: {id}"));
                }
                return gid;
            }).ToList();

            // Call application-layer service
            var products = await _productService.GetByIdsAsync(ids);

            _logger.LogInformation(
                "Products retrieved successfully. requested={Requested}, found={Found}",
                ids.Count, products.Count);

            // Map domain entities → protobuf response
            var reply = new GetProductsByIdsReply();
            reply.Products.AddRange(products.Select(p => new ECommerce.GrpcContracts.Products.Product
            {
                Id = p.Id.ToString(),
                Name = p.Name ?? string.Empty,

                // decimal → string to avoid precision loss
                Price = p.Price.ToString(CultureInfo.InvariantCulture),
                DiscountedPrice = p.DiscountedPrice.ToString(CultureInfo.InvariantCulture),

                StockQuantity = p.StockQuantity
            }));

            return reply;
        }

        // ============================================================================
        // 2. CHECK PRODUCTS AVAILABILITY
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Validate stock before order placement
        //   - Fail fast if product or quantity is invalid
        //
        // Flow:
        //   (a) Validate request items
        //   (b) Convert to ProductStockInfoRequestDTO
        //   (c) Call inventory service
        //   (d) Map results → protobuf response
        // ============================================================================
        public override async Task<CheckProductsAvailabilityReply> CheckProductsAvailability(
            CheckProductsAvailabilityRequest request,
            ServerCallContext context)
        {
            _logger.LogInformation(
                "gRPC CheckProductsAvailability called. itemCount={Count}",
                request.Items?.Count ?? 0);

            if (request.Items == null || request.Items.Count == 0)
            {
                _logger.LogWarning("CheckProductsAvailability failed: items missing.");
                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "items required."));
            }

            // Convert protobuf items → application DTOs
            var dto = request.Items.Select(i =>
            {
                if (!Guid.TryParse(i.ProductId, out var pid))
                {
                    _logger.LogWarning(
                        "Invalid product_id in stock check: {ProductId}", i.ProductId);
                    throw new RpcException(
                        new Status(StatusCode.InvalidArgument,
                                   $"Invalid product_id: {i.ProductId}"));
                }

                return new ProductStockInfoRequestDTO
                {
                    ProductId = pid,
                    Quantity = i.Quantity
                };
            }).ToList();

            // Call inventory service
            var results = await _inventoryService.VerifyStockForProductsAsync(dto);

            _logger.LogInformation(
                "Stock verification completed. itemCount={Count}",
                results.Count);

            // Map application results → protobuf response
            var reply = new CheckProductsAvailabilityReply();
            reply.Results.AddRange(results.Select(r => new StockCheckResult
            {
                ProductId = r.ProductId.ToString(),
                IsValidProduct = r.IsValidProduct,
                IsQuantityAvailable = r.IsQuantityAvailable,
                AvailableQuantity = r.AvailableQuantity
            }));

            return reply;
        }

        // ============================================================================
        // 3. INCREASE STOCK BULK
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Increase inventory in bulk
        //   - Used during order cancellation or returns
        // ============================================================================
        public override async Task<StockBulkUpdateReply> IncreaseStockBulk(
            StockBulkUpdateRequest request,
            ServerCallContext context)
        {
            _logger.LogInformation("gRPC IncreaseStockBulk called.");

            var updates = ToInventoryUpdates(request);

            await _inventoryService.IncreaseStockBulkAsync(updates);

            _logger.LogInformation(
                "Stock increased successfully. updateCount={Count}",
                updates.Count);

            return new StockBulkUpdateReply
            {
                Success = true,
                Message = "Stock increased."
            };
        }

        // ============================================================================
        // 4. DECREASE STOCK BULK
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Decrease inventory after successful order placement
        // ============================================================================
        public override async Task<StockBulkUpdateReply> DecreaseStockBulk(
            StockBulkUpdateRequest request,
            ServerCallContext context)
        {
            _logger.LogInformation("gRPC DecreaseStockBulk called.");

            var updates = ToInventoryUpdates(request);

            await _inventoryService.DecreaseStockBulkAsync(updates);

            _logger.LogInformation(
                "Stock decreased successfully. updateCount={Count}",
                updates.Count);

            return new StockBulkUpdateReply
            {
                Success = true,
                Message = "Stock decreased."
            };
        }

        // ============================================================================
        // HELPER: Convert StockBulkUpdateRequest → InventoryUpdateDTO
        // ----------------------------------------------------------------------------
        // Responsibility:
        //   - Validate incoming gRPC data
        //   - Convert string IDs → Guid
        //   - Return application-layer DTOs
        // ============================================================================
        private static List<InventoryUpdateDTO> ToInventoryUpdates(
            StockBulkUpdateRequest request)
        {
            if (request.Updates == null || request.Updates.Count == 0)
                throw new RpcException(
                    new Status(StatusCode.InvalidArgument, "updates required."));

            return request.Updates.Select(u =>
            {
                if (!Guid.TryParse(u.ProductId, out var pid))
                    throw new RpcException(
                        new Status(StatusCode.InvalidArgument,
                                   $"Invalid product_id: {u.ProductId}"));

                return new InventoryUpdateDTO
                {
                    ProductId = pid,
                    Quantity = u.Quantity
                };
            }).ToList();
        }
    }
}
Wire in ProductService.API/Program.cs:
  • builder.Services.AddGrpc();
  • app.MapGrpcService<ProductService.API.GrpcServices.ProductGrpcService>();

Step-3: Client-side (OrderService.Infrastructure):

So far, we have:

  • Defined shared gRPC contracts (.proto files)
  • Added gRPC servers to UserService and ProductService

Now, the final step is to enable OrderService to call those gRPC services. This is done from the Order Service Infrastructure layer, because:

  • External service communication is an infrastructure concern
  • The Domain and Application layers must remain protocol-agnostic
Why gRPC Clients Belong in the Infrastructure Layer

The Infrastructure layer is responsible for:

  • Talking to external systems
  • Handling transport protocols (REST, gRPC, messaging, etc.)
  • Translating external data into internal DTOs

By placing gRPC clients here:

  • The Application layer stays clean
  • We can switch implementations (REST ↔ gRPC) without changing business logic
  • Clean Architecture principles are preserved
Install Required gRPC Packages

Inside the OrderService.Infrastructure project, please install the following NuGet packages by executing the commands below in Package Manager Console:

  • Install-Package Grpc.Net.ClientFactory
  • Install-Package Grpc.Core.Api
  • Install-Package Google.Protobuf
  • Install-Package Grpc.Tools
Why These Packages Are Needed
  • Grpc.Net.ClientFactory: Integrates gRPC clients with ASP.NET Core’s HttpClientFactory.
      • Enables DI
      • Supports retries, logging, DNS refresh, etc.
  • Grpc.Core.Api: Provides core gRPC types such as:
      • Metadata
      • RpcException
      • StatusCode
  • Google.Protobuf: Required to work with protobuf-generated message classes.
  • Grpc.Tools: Generates client stubs from .proto files.
Add References to both Proto Files:

The OrderService must generate client stubs, not server code. Therefore, we reference both proto files with: GrpcServices=”Client”

Create GrpcClients Folder

Inside the OrderService.Infrastructure project, create a folder named: GrpcClients. This folder will contain:

  • gRPC client implementations
  • One client per external microservice

This naming makes intent clear and keeps infrastructure code organized.

UserServiceGrpcClient (Client Implementation)

This class:

  • Lives in OrderService.Infrastructure
  • Implements IUserServiceClient
  • Replaces REST calls with gRPC calls transparently
Key Responsibilities of UserServiceGrpcClient
  • Call UserService gRPC endpoints
  • Pass authentication metadata (JWT)
  • Convert protobuf messages → OrderService DTOs
  • Log request and response lifecycle
Why We Implement an Interface

UserServiceGrpcClient implements IUserServiceClient, which means:

  • Application layer code does not know it is using gRPC
  • Switching between REST and gRPC requires no application changes
  • Dependency Inversion Principle is respected

So, add a class file named UserServiceGrpcClient.cs within the GrpcClients folder, and copy-paste the following code. The following code is self-explanatory; please read the comment lines for better understanding.

using ECommerce.GrpcContracts.Users;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using OrderService.Contracts.DTOs;
using OrderService.Contracts.ExternalServices;

namespace OrderService.Infrastructure.GrpcClients
{
    // ============================================================================
    // gRPC Client Implementation for UserService
    // ----------------------------------------------------------------------------
    // This class:
    //   - Lives in OrderService.Infrastructure
    //   - Implements IUserServiceClient
    //   - Communicates with UserService via gRPC
    //
    // Responsibilities:
    //   - Call UserService gRPC endpoints
    //   - Pass authentication metadata (JWT)
    //   - Map protobuf messages → OrderService DTOs
    // ============================================================================
    public sealed class UserServiceGrpcClient : IUserServiceClient
    {
        private readonly UserGrpc.UserGrpcClient _client;
        private readonly ILogger<UserServiceGrpcClient> _logger;

        // ------------------------------------------------------------------------
        // Constructor
        // ------------------------------------------------------------------------
        // UserGrpcClient is generated by gRPC tooling
        // ILogger is injected by ASP.NET Core
        public UserServiceGrpcClient(
            UserGrpc.UserGrpcClient client,
            ILogger<UserServiceGrpcClient> logger)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        // ============================================================================
        // 1. CHECK IF USER EXISTS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Lightweight validation before order creation
        //   - Avoid unnecessary work if user is invalid
        // ============================================================================
        public async Task<bool> UserExistsAsync(Guid userId, string accessToken)
        {
            _logger.LogInformation(
                "Calling UserService.UserExists via gRPC. user_id={UserId}",
                userId);

            var reply = await _client.UserExistsAsync(
                new UserExistsRequest
                {
                    // Guid → string (protobuf has no Guid type)
                    UserId = userId.ToString()
                },
                headers: BuildHeaders(accessToken));

            _logger.LogInformation(
                "UserExists completed. user_id={UserId}, exists={Exists}",
                userId, reply.Exists);

            return reply.Exists;
        }

        // ============================================================================
        // 2. GET USER PROFILE BY ID
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch user profile details
        //   - Used during order creation and validation
        // ============================================================================
        public async Task<UserDTO?> GetUserByIdAsync(Guid userId, string accessToken)
        {
            _logger.LogInformation(
                "Calling UserService.GetUserById via gRPC. user_id={UserId}",
                userId);

            var reply = await _client.GetUserByIdAsync(
                new GetUserByIdRequest
                {
                    UserId = userId.ToString()
                },
                headers: BuildHeaders(accessToken));

            // If user not found → return null
            if (!reply.Found || reply.User == null)
            {
                _logger.LogInformation(
                    "User not found in UserService. user_id={UserId}",
                    userId);

                return null;
            }

            _logger.LogInformation(
                "User profile retrieved successfully. user_id={UserId}",
                userId);

            // Map protobuf User → OrderService UserDTO
            return new UserDTO
            {
                Id = Guid.Parse(reply.User.Id),
                FullName = reply.User.FullName,
                Email = reply.User.Email,
                PhoneNumber = reply.User.PhoneNumber
            };
        }

        // ============================================================================
        // 3. GET USER ADDRESS BY ID
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch a specific user address (shipping/billing)
        //   - Used during order placement
        // ============================================================================
        public async Task<AddressDTO?> GetUserAddressByIdAsync(
            Guid userId,
            Guid addressId,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling UserService.GetUserAddressById via gRPC. user_id={UserId}, address_id={AddressId}",
                userId, addressId);

            var reply = await _client.GetUserAddressByIdAsync(
                new GetUserAddressByIdRequest
                {
                    UserId = userId.ToString(),
                    AddressId = addressId.ToString()
                },
                headers: BuildHeaders(accessToken));

            // Address not found
            if (!reply.Found || reply.Address == null)
            {
                _logger.LogInformation(
                    "Address not found. user_id={UserId}, address_id={AddressId}",
                    userId, addressId);

                return null;
            }

            _logger.LogInformation(
                "Address retrieved successfully. user_id={UserId}, address_id={AddressId}",
                userId, addressId);

            // Map protobuf Address → OrderService AddressDTO
            return new AddressDTO
            {
                Id = Guid.Parse(reply.Address.Id),
                UserId = Guid.Parse(reply.Address.UserId),
                AddressLine1 = reply.Address.AddressLine1,
                AddressLine2 =
                    string.IsNullOrWhiteSpace(reply.Address.AddressLine2)
                        ? null
                        : reply.Address.AddressLine2,
                City = reply.Address.City,
                State = reply.Address.State,
                PostalCode = reply.Address.PostalCode,
                Country = reply.Address.Country,
                IsDefaultBilling = reply.Address.IsDefaultBilling,
                IsDefaultShipping = reply.Address.IsDefaultShipping
            };
        }

        // ============================================================================
        // 4. SAVE OR UPDATE USER ADDRESS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Create or update a user address in UserService
        // ============================================================================
        public async Task<Guid?> SaveOrUpdateAddressAsync(
            AddressDTO addressDto,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling UserService.SaveOrUpdateAddress via gRPC. user_id={UserId}, address_id={AddressId}",
                addressDto.UserId,
                addressDto.Id == Guid.Empty ? "NEW" : addressDto.Id);

            var reply = await _client.SaveOrUpdateAddressAsync(
                new SaveOrUpdateAddressRequest
                {
                    Address = new ECommerce.GrpcContracts.Users.Address
                    {
                        // Empty string indicates "new address"
                        Id = addressDto.Id == Guid.Empty
                                ? string.Empty
                                : addressDto.Id.ToString(),

                        UserId = addressDto.UserId.ToString(),
                        AddressLine1 = addressDto.AddressLine1,
                        AddressLine2 = addressDto.AddressLine2 ?? string.Empty,
                        City = addressDto.City,
                        State = addressDto.State,
                        PostalCode = addressDto.PostalCode,
                        Country = addressDto.Country,
                        IsDefaultBilling = addressDto.IsDefaultBilling,
                        IsDefaultShipping = addressDto.IsDefaultShipping
                    }
                },
                headers: BuildHeaders(accessToken));

            // Convert returned AddressId → Guid
            if (Guid.TryParse(reply.AddressId, out var id))
            {
                _logger.LogInformation(
                    "Address saved successfully. address_id={AddressId}",
                    id);

                return id;
            }

            _logger.LogWarning(
                "SaveOrUpdateAddress returned invalid address_id. value={Value}",
                reply.AddressId);

            return null;
        }

        // ============================================================================
        // HELPER: BUILD gRPC METADATA HEADERS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Attach Authorization header for secured gRPC calls
        //
        // Notes:
        //   - gRPC headers must be lowercase
        //   - JWT is passed as Bearer token
        // ============================================================================
        private static Metadata BuildHeaders(string accessToken)
        {
            var headers = new Metadata();

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                headers.Add("authorization", $"Bearer {accessToken}");
            }

            return headers;
        }
    }
}

ProductServiceGrpcClient implements IProductServiceClient

This class performs high-frequency, performance-critical calls to ProductService. Add a class file named ProductServiceGrpcClient.cs within the GrpcClients folder, and copy-paste the following code.

using ECommerce.GrpcContracts.Products;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using OrderService.Contracts.DTOs;
using OrderService.Contracts.ExternalServices;
using System.Globalization;

namespace OrderService.Infrastructure.GrpcClients
{
    // ============================================================================
    // gRPC Client Implementation for ProductService
    // ----------------------------------------------------------------------------
    // This class:
    //   - Lives in OrderService.Infrastructure
    //   - Implements IProductServiceClient
    //   - Communicates with ProductService via gRPC
    //
    // Responsibilities:
    //   - Call ProductService gRPC endpoints
    //   - Pass authorization metadata (JWT)
    //   - Convert protobuf messages to OrderService DTOs
    // ============================================================================
    public sealed class ProductServiceGrpcClient : IProductServiceClient
    {
        private readonly ProductGrpc.ProductGrpcClient _client;
        private readonly ILogger<ProductServiceGrpcClient> _logger;

        // ------------------------------------------------------------------------
        // Constructor
        // ------------------------------------------------------------------------
        // ProductGrpcClient is generated by gRPC tooling.
        // ILogger is injected by ASP.NET Core DI.
        public ProductServiceGrpcClient(
            ProductGrpc.ProductGrpcClient client,
            ILogger<ProductServiceGrpcClient> logger)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        // ============================================================================
        // 1. GET SINGLE PRODUCT BY ID
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Convenience method for fetching a single product
        //
        // NOTE:
        //   - Internally delegates to bulk GetProductsByIdsAsync
        //   - Keeps logic DRY and consistent
        // ============================================================================
        public async Task<ProductDTO?> GetProductByIdAsync(Guid productId)
        {
            _logger.LogInformation(
                "Calling ProductService.GetProductById via gRPC. product_id={ProductId}",
                productId);

            // Interface does not accept token here.
            // If ProductService requires auth, either:
            //   - Change interface to accept token, OR
            //   - Keep REST fallback for this method
            var list = await GetProductsByIdsAsync(
                new List<Guid> { productId },
                accessToken: string.Empty);

            var product = list?.FirstOrDefault();

            _logger.LogInformation(
                product == null
                    ? "Product not found. product_id={ProductId}"
                    : "Product retrieved successfully. product_id={ProductId}",
                productId);

            return product;
        }

        // ============================================================================
        // 2. GET PRODUCTS BY IDS (BULK)
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Fetch multiple products in one gRPC call
        //   - Used during order creation for pricing & validation
        // ============================================================================
        public async Task<List<ProductDTO>?> GetProductsByIdsAsync(
            List<Guid> productIds,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling ProductService.GetProductsByIds via gRPC. count={Count}",
                productIds.Count);

            // Build protobuf request
            var req = new GetProductsByIdsRequest();
            req.ProductIds.AddRange(productIds.Select(x => x.ToString()));

            // Call gRPC endpoint with auth headers
            var reply = await _client.GetProductsByIdsAsync(
                req,
                headers: BuildHeaders(accessToken));

            _logger.LogInformation(
                "Products retrieved successfully. count={Count}",
                reply.Products.Count);

            // Map protobuf Product → OrderService ProductDTO
            return reply.Products.Select(p => new ProductDTO
            {
                Id = Guid.Parse(p.Id),
                Name = p.Name,

                // string → decimal (InvariantCulture to avoid precision issues)
                Price = decimal.Parse(p.Price, CultureInfo.InvariantCulture),
                DiscountedPrice = decimal.Parse(p.DiscountedPrice, CultureInfo.InvariantCulture),

                StockQuantity = p.StockQuantity
            }).ToList();
        }

        // ============================================================================
        // 3. CHECK PRODUCTS AVAILABILITY
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Validate stock availability before order placement
        //   - High-frequency, performance-critical call
        // ============================================================================
        public async Task<List<ProductStockVerificationResponseDTO>?> CheckProductsAvailabilityAsync(
            List<ProductStockVerificationRequestDTO> requestedItems,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling ProductService.CheckProductsAvailability via gRPC. itemCount={Count}",
                requestedItems.Count);

            // Build protobuf request
            var req = new CheckProductsAvailabilityRequest();
            req.Items.AddRange(requestedItems.Select(i => new StockCheckItem
            {
                ProductId = i.ProductId.ToString(),
                Quantity = i.Quantity
            }));

            // Call gRPC endpoint
            var reply = await _client.CheckProductsAvailabilityAsync(
                req,
                headers: BuildHeaders(accessToken));

            _logger.LogInformation(
                "Stock availability check completed. resultCount={Count}",
                reply.Results.Count);

            // Map protobuf results → OrderService DTOs
            return reply.Results.Select(r => new ProductStockVerificationResponseDTO
            {
                ProductId = Guid.Parse(r.ProductId),
                IsValidProduct = r.IsValidProduct,
                IsQuantityAvailable = r.IsQuantityAvailable,
                AvailableQuantity = r.AvailableQuantity
            }).ToList();
        }

        // ============================================================================
        // 4. INCREASE STOCK (BULK)
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Increase inventory quantities
        //   - Used during order cancellation / returns
        // ============================================================================
        public async Task<bool> IncreaseStockBulkAsync(
            IEnumerable<UpdateStockRequestDTO> stockUpdates,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling ProductService.IncreaseStockBulk via gRPC.");

            var req = new StockBulkUpdateRequest();
            req.Updates.AddRange(stockUpdates.Select(u => new StockUpdate
            {
                ProductId = u.ProductId.ToString(),
                Quantity = u.Quantity
            }));

            var reply = await _client.IncreaseStockBulkAsync(
                req,
                headers: BuildHeaders(accessToken));

            _logger.LogInformation(
                "IncreaseStockBulk completed. success={Success}",
                reply.Success);

            return reply.Success;
        }

        // ============================================================================
        // 5. DECREASE STOCK (BULK)
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Decrease inventory after successful order placement
        // ============================================================================
        public async Task<bool> DecreaseStockBulkAsync(
            IEnumerable<UpdateStockRequestDTO> stockUpdates,
            string accessToken)
        {
            _logger.LogInformation(
                "Calling ProductService.DecreaseStockBulk via gRPC.");

            var req = new StockBulkUpdateRequest();
            req.Updates.AddRange(stockUpdates.Select(u => new StockUpdate
            {
                ProductId = u.ProductId.ToString(),
                Quantity = u.Quantity
            }));

            var reply = await _client.DecreaseStockBulkAsync(
                req,
                headers: BuildHeaders(accessToken));

            _logger.LogInformation(
                "DecreaseStockBulk completed. success={Success}",
                reply.Success);

            return reply.Success;
        }

        // ============================================================================
        // HELPER: BUILD gRPC METADATA HEADERS
        // ----------------------------------------------------------------------------
        // Purpose:
        //   - Attach JWT token for authenticated gRPC calls
        //
        // Important:
        //   - gRPC header keys must be lowercase
        // ============================================================================
        private static Metadata BuildHeaders(string accessToken)
        {
            var headers = new Metadata();

            if (!string.IsNullOrWhiteSpace(accessToken))
            {
                headers.Add("authorization", $"Bearer {accessToken}");
            }

            return headers;
        }
    }
}

DI Wiring (OrderService.Infrastructure): swap REST clients to gRPC clients

We replace REST clients with gRPC clients without touching application code. So, please modify the InfrastructureServiceRegistration class as follows.

using ECommerce.GrpcContracts.Products;
using ECommerce.GrpcContracts.Users;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OrderService.Contracts.ExternalServices;
using OrderService.Domain.Repositories;
using OrderService.Infrastructure.ExternalServices;
using OrderService.Infrastructure.GrpcClients;
using OrderService.Infrastructure.Repositories;

namespace OrderService.Infrastructure.DependencyInjection
{
    // ============================================================================
    // Infrastructure Service Registration
    // ----------------------------------------------------------------------------
    // This class is responsible for wiring up ALL infrastructure dependencies:
    //   - gRPC clients
    //   - REST HttpClients
    //   - Repository implementations
    // ============================================================================
    public static class InfrastructureServiceRegistration
    {
        // ------------------------------------------------------------------------
        // Extension method to register infrastructure services
        // ------------------------------------------------------------------------
        public static IServiceCollection AddInfrastructureServices(
            this IServiceCollection services,
            IConfiguration configuration)
        {
            // ============================================================
            // USER SERVICE & PRODUCT SERVICE → gRPC CLIENTS
            // ============================================================
            // These services are:
            //   - High-frequency
            //   - Internal
            //   - Synchronous
            //
            // gRPC is chosen for:
            //   - Better performance
            //   - Lower latency
            //   - Strong contracts
            // ============================================================

            // -----------------------------
            // UserService gRPC client
            // -----------------------------
            services.AddGrpcClient<UserGrpc.UserGrpcClient>(o =>
            {
                // Read UserService base URL from configuration
                // Example:
                //   "ExternalServices:UserServiceUrl": "https://localhost:5001"
                var baseUrl = configuration["ExternalServices:UserServiceUrl"]
                    ?? throw new ArgumentNullException("UserServiceUrl not configured");

                // Configure gRPC client endpoint
                o.Address = new Uri(baseUrl);
            });

            // -----------------------------
            // ProductService gRPC client
            // -----------------------------
            services.AddGrpcClient<ProductGrpc.ProductGrpcClient>(o =>
            {
                var baseUrl = configuration["ExternalServices:ProductServiceUrl"]
                    ?? throw new ArgumentNullException("ProductServiceUrl not configured");

                o.Address = new Uri(baseUrl);
            });

            // ============================================================
            // PAYMENT & NOTIFICATION → REST HTTP CLIENTS
            // ============================================================
            // These services are:
            //   - Possibly external
            //   - Lower frequency
            //   - Asynchronous / eventual consistency
            //
            // REST is intentionally retained here.
            // ============================================================

            // -----------------------------
            // PaymentService REST client
            // -----------------------------
            services.AddHttpClient("PaymentServiceClient", client =>
            {
                var baseUrl = configuration["ExternalServices:PaymentServiceUrl"]
                    ?? throw new ArgumentNullException("PaymentServiceUrl not configured");

                client.BaseAddress = new Uri(baseUrl);
            });

            // -----------------------------
            // NotificationService REST client
            // -----------------------------
            services.AddHttpClient("NotificationServiceClient", client =>
            {
                var baseUrl = configuration["ExternalServices:NotificationServiceUrl"]
                    ?? throw new ArgumentNullException("NotificationServiceUrl not configured");

                client.BaseAddress = new Uri(baseUrl);
            });

            // -----------------------------
            // Swap REST with gRPC seamlessly
            // -----------------------------
            services.AddScoped<IUserServiceClient, UserServiceGrpcClient>();
            // OrderService now calls UserService via gRPC

            services.AddScoped<IProductServiceClient, ProductServiceGrpcClient>();
            // OrderService now calls ProductService via gRPC

            // -----------------------------
            // REST-based external services
            // -----------------------------
            services.AddScoped<IPaymentServiceClient, PaymentServiceClient>();
            services.AddScoped<INotificationServiceClient, NotificationServiceClient>();

            // ============================================================
            // DOMAIN REPOSITORIES (UNCHANGED)
            // ============================================================
            // These are internal data access abstractions.
            // ============================================================

            services.AddScoped<ICancellationRepository, CancellationRepository>();
            services.AddScoped<ICartRepository, CartRepository>();
            services.AddScoped<IMasterDataRepository, MasterDataRepository>();
            services.AddScoped<IOrderRepository, OrderRepository>();
            services.AddScoped<IRefundRepository, RefundRepository>();
            services.AddScoped<IReturnRepository, ReturnRepository>();
            services.AddScoped<IShipmentRepository, ShipmentRepository>();

            return services;
        }
    }
}
Run Consul in Development Mode

Please run the following command in the command prompt.

consul agent -dev -client=0.0.0.0 -ui -node=consul-dev

Note: Keep this command window open while working with your microservices. If you close it, Consul stops and services cannot register or be discovered.

Open Consul UI

Once Consul is running, we can monitor everything from the browser. Open your browser and navigate to: http://localhost:8500

Run all Microservices:
Test the order Creation Endpoint:
Who Generates the Code and When?
  • Grpc.Tools run at build time
  • It reads your .proto files
  • It generates C# code automatically
  • You never write or edit this generated code

Think of it as: .proto file is the source of truth → C# code is just a generated result.

Location of Generated Code: obj\Debug\net8.0\

In this implementation, we saw how gRPC helps microservices communicate faster and more efficiently. Using gRPC between internal services such as Order, User, and Product makes the system faster and more reliable. At the same time, REST APIs are still used for client requests, which keeps the system flexible. The key point is that gRPC is not a replacement for REST, but a better choice for internal service communication in real-world microservice applications.

Registration Open – Angular Online Training

New Batch Starts: 19th January, 2026
Session Time: 8:30 PM – 10:00 PM IST

Advance your career with our expert-led, hands-on live training program. Get complete course details, the syllabus, and Zoom credentials for demo sessions via the links below.

Contact: +91 70218 01173 (Call / WhatsApp)

1 thought on “Implementing gRPC in ASP.NET Core”

  1. blank

    🎥 Watch This Step-by-Step Tutorial on gRPC in ASP.NET Core Microservices
    Want to learn how to implement high-performance internal communication between your microservices using gRPC? In this in-depth video, we cover everything from .proto contracts to real-time service-to-service communication in .NET Core.

    ▶️ Watch Now on YouTube: gRPC in ASP.NET Core Microservices

    📌 Learn the real-world architecture, best practices, and how gRPC outperforms REST in internal microservices!

Leave a Reply

Your email address will not be published. Required fields are marked *