Building a Modern Microservice Architecture with .NET, gRPC, Dapr, and Blazor

In today’s cloud-native world, breaking down monolithic applications into smaller, more manageable pieces, microservices has become the norm. Microservices enable flexibilityscalability, and resilience.

In this article, we will walk through the steps of building an e-commerce platform using microservices. The architecture will leverage technologies like gRPCDaprcontainerization, and Blazor for the frontend.

What is Microservices?

Microservices divide an application into smallloosely coupled services, each focused on a single business function. In the context of an e-commerce platform, you might have separate services like:

  • ProductService to manage product information
  • OrderService to handle orders and payments
  • InventoryService to track product stock levels

By using microservices, you can scale each service independently, deploy them separately, and ensure that each service can evolve without affecting others. This leads to faster development cycles, better fault isolation, and improved resilience.

Key Concepts

  • Single Responsibility: Each microservice focuses on one function (e.g., user management, order processing).
  • Independently Deployable: Teams can develop, test, deploy, and scale each service independently.
  • Decentralized Data Management: Each service owns its own database to reduce dependencies and avoid shared data sources.
  • Technology Agnostic: Services can be written in different languages and use different data stores.

Benefits

  • Scalability: Scale only the services that need it.
  • Flexibility: Use different tech stacks per service.
  • Resilience: Failure in one service doesn’t crash the whole system.
  • Faster Development: Teams can work in parallel and deploy continuously.

Using gRPC, Dapr, and Message Brokers

gRPC: Lightweight Communication for Distributed Systems

gRPC is an efficient, high-performance protocol developed by Google. It’s ideal for internal service-to-service communication, especially in microservices architectures. It provides:

  • High performance: Uses HTTP/2 for multiplexed streams.
  • Type safety: Defined in Protocol Buffers (Protobuf), which ensures that data exchanged between services is strongly typed.
  • Streaming: Supports bidirectional streaming, ideal for handling real-time data exchanges.

For example, OrderService receives orders via gRPC, allowing it to quickly process large amounts of data from clients and communicate with other services in the backend.

Dapr: Communication and State for Distributed Apps

Dapr (Distributed Application Runtime) simplifies the development of distributed applications by providing:

  • Service Invocation: Makes it easy for microservices to call each other over HTTP/gRPC.
  • Pub/Sub Messaging: Enables event-driven architecture. Services can publish and subscribe to events without tightly coupling the logic between them.
  • State Management: Stores key-value pairs (like inventory data) in a distributed, consistent way.

For example, OrderService publishes an order.created event to InventoryService, and InventoryService responds by updating stock levels and publishing an inventory.updated event. All of this communication is handled by Dapr.

Message Brokers (RabbitMQ or Azure Service Bus): Reliable Messaging Between Decoupled Services

While Dapr’s pub/sub component abstracts messaging middleware like RabbitMQ or Azure Service Bus, these systems are still relevant for large-scale applications. They provide reliable messaging between services. Dapr can plug into either, depending on your infrastructure.

Project Architecture

Blazor Frontend (SPA)

The Blazor WebAssembly app will serve as the frontend for our e-commerce platform. Blazor enables you to build richinteractive web UIs with C# instead of JavaScript. The frontend communicates with the backend via:

  • RESTful API calls to ProductService to fetch product data.
  • gRPC calls to OrderService to place orders.

API Gateway

The API Gateway is the entry point to the system. It routes client requests to the appropriate service:

  • ProductService (REST for browsing products)
  • OrderService (gRPC for placing orders)

The API Gateway can be implemented using YARP (Yet Another Reverse Proxy) or as a simple .NET service, routing requests based on the type of communication needed.

ProductService

The ProductService handles product-related tasks:

  • Exposes REST API endpoints for browsing and searching products.
  • Stores product data in a database (SQL or NoSQL) using Entity Framework Core.

Because this service needs to be accessed externally (via the Blazor frontend), it does not use gRPCREST APIs are more suitable for external communications where JSON is typically used.

OrderService

The OrderService handles everything related to order creation:

  • Receives Orders via gRPC from the Blazor frontend via the API Gateway.
  • Publishes an order.created event to Dapr’s pub/sub component for other services to react to.
  • Subscribes to inventory.updated events to track stock levels and update the order status.

InventoryService

InventoryService uses Dapr’s state management to persist inventory data in a distributed store (e.g., Redis, Cosmos DB).

  • Subscribes to order.created events from Dapr and updates inventory levels accordingly.
  • Publishes inventory.updated events once stock is updated, so OrderService can react and finalize orders.

Putting the Pieces Together

Here’s a quick walkthrough of how the services interact in the system:

1️⃣ User adds items to the cart (via Blazor → ProductService via REST)

2️⃣ User places an order (via Blazor → OrderService via gRPC)

3️⃣ OrderService:

  • Saves the order in the database
  • Publishes an order.created event to Dapr Pub/Sub

4️⃣ InventoryService:

  • Subscribes to order.created event
  • Updates inventory levels in the state store
  • Publishes inventory.updated event when done

5️⃣ OrderService:

  • Subscribes to inventory.updated event
  • Updates the order status based on inventory changes

ProductService: Exposing Product Information via REST API

This service will expose a REST API endpoint that returns product details. For simplicity, we’ll use ASP.NET Core with Entity Framework Core for data persistence.

This service is simple: it exposes two endpoints:

  1. GET /api/products to retrieve all products.
  2. GET /api/products/{id} to retrieve a single product.
// ProductService/Controllers/ProductController.cs

using Microsoft.AspNetCore.Mvc;
using ProductService.Data;
using ProductService.Models;

namespace ProductService.Controllers
{
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
private readonly ApplicationDbContext _context;

public ProductController(ApplicationDbContext context)
{
_context = context;
}

// GET api/products/{id}
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}

// GET api/products
[HttpGet]
public async Task<IActionResult> GetProducts()
{
var products = await _context.Products.ToListAsync();
return Ok(products);
}
}
}

ProductService: Data Context and Model

// ProductService/Data/ApplicationDbContext.cs

using Microsoft.EntityFrameworkCore;
using ProductService.Models;

namespace ProductService.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }

public DbSet<Product> Products { get; set; }
}
}

// ProductService/Models/Product.cs
namespace ProductService.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
}

OrderService: Handling the Order Creation via gRPC

OrderService publishes an order.created event via Dapr Pub/Sub, which InventoryService listens to.

gRPC and Event Publishing

This is a Protocol Buffers (proto3) file used by gRPC to define the structure of messages and services for remote procedure calls.

This is the gRPC definition for placing an order. The PlaceOrder method receives a productId and quantity and returns an order ID and status.

// OrderService/Protos/order.proto

syntax = "proto3";

package order;

service OrderService {
rpc PlaceOrder (OrderRequest) returns (OrderResponse);
}

message OrderRequest {
int32 productId = 1;
int32 quantity = 2;
}

message OrderResponse {
int32 orderId = 1;
string status = 2;
}

gRPC Implementation

When an order is placed, it creates an order record in the database and publishes an order.created event to Dapr Pub/Sub. The InventoryService will be subscribed to this event.

// OrderService/Services/OrderService.cs

using Grpc.Core;
using Dapr.Client;
using OrderService.Protos;
using OrderService.Models;

public class OrderService : OrderService.OrderServiceBase
{
private readonly DaprClient _daprClient;
private readonly ApplicationDbContext _context;

public OrderService(DaprClient daprClient, ApplicationDbContext context)
{
_daprClient = daprClient;
_context = context;
}

public override async Task<OrderResponse> PlaceOrder(OrderRequest request, ServerCallContext context)
{
// Retrieve product from ProductService
var product = await GetProductById(request.ProductId);

if (product == null || product.Stock < request.Quantity)
{
return new OrderResponse { Status = "Insufficient stock" };
}

// Create order
var order = new Order
{
ProductId = request.ProductId,
Quantity = request.Quantity,
Status = "Pending"
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();

// Publish the order created event
await _daprClient.PublishEventAsync("pubsub", "order.created", order);

return new OrderResponse
{
OrderId = order.Id,
Status = "Order created"
};
}

private async Task<Product> GetProductById(int productId)
{
// Simulate gRPC call to ProductService
var channel = GrpcChannel.ForAddress("http://productservice");
var client = new ProductService.ProductServiceClient(channel);
var response = await client.GetProductAsync(new GetProductRequest { Id = productId });
return response != null ? new Product { Id = response.Id, Name = response.Name, Price = response.Price, Stock = response.Stock } : null;
}
}

OrderService Subscribing to inventory.updated

OrderService uses [Topic("pubsub", "inventory.updated")] to subscribe to the inventory.updated event. When InventoryService publishes this event after updating the inventory, OrderService can then update the order status to “Shipped” (or any other status).

// OrderService/Services/OrderService.cs

[Topic("pubsub", "inventory.updated")] // Dapr event subscription
public async Task HandleInventoryUpdatedAsync(Product product)
{
var order = await _context.Orders
.Where(o => o.ProductId == product.Id && o.Status == "Pending")
.FirstOrDefaultAsync();

if (order != null)
{
// If inventory was updated successfully, mark the order as "Shipped"
order.Status = "Shipped";
_context.Orders.Update(order);
await _context.SaveChangesAsync();
}
}

InventoryService: Handling Inventory and Events

The InventoryService listens for the order.created event and updates the inventory when an order is placed.

InventoryService: Event Subscription and Inventory Update

// InventoryService/Services/InventoryService.cs

using Dapr.Client;
using InventoryService.Models;
using InventoryService.Protos;

public class InventoryService
{
private readonly DaprClient _daprClient;
private readonly ApplicationDbContext _context;

public InventoryService(DaprClient daprClient, ApplicationDbContext context)
{
_daprClient = daprClient;
_context = context;
}

// Subscribe to the 'order.created' event
[Topic("pubsub", "order.created")]
public async Task HandleOrderCreatedAsync(Order order)
{
var product = await _context.Products.FindAsync(order.ProductId);

if (product != null && product.Stock >= order.Quantity)
{
product.Stock -= order.Quantity; // Reduce stock
_context.Products.Update(product);
await _context.SaveChangesAsync();

// Publish inventory updated event
await _daprClient.PublishEventAsync("pubsub", "inventory.updated", product);
}
else
{
// Handle insufficient stock scenario (optional)
}
}
}

InventoryService: Inventory Update Event

Once the stock is updated, the InventoryService publishes an inventory.updated event. This is an event that OrderService subscribes to for order status updates.

InventoryService Subscribing to order.created

InventoryService uses the [Topic("pubsub", "order.created")] attribute to subscribe to the event. When the event is triggered, it reduces the stock and publishes the inventory.updated event.

// InventoryService/Services/InventoryService.cs

[Topic("pubsub", "order.created")] // Dapr event subscription
public async Task HandleOrderCreatedAsync(Order order)
{
var product = await _context.Products.FindAsync(order.ProductId);

if (product != null && product.Stock >= order.Quantity)
{
product.Stock -= order.Quantity; // Reduce stock
_context.Products.Update(product);
await _context.SaveChangesAsync();

// Publish inventory.updated event via Dapr Pub/Sub
await _daprClient.PublishEventAsync("pubsub", "inventory.updated", product);
}
else
{
// Handle insufficient stock case (optional)
}
}

Blazor Front End: Displaying Orders and Products

In the Blazor frontend, you will interact with the OrderService to place orders, and then display the status of the orders and products. We’ll show how to integrate the frontend with these services.

Blazor Client Setup:

  1. Place Order: The Blazor frontend will send a gRPC request to OrderService to place an order.
  2. View Products: The Blazor frontend can make REST API calls to ProductService to view products.
  3. Display Order Status: Once the order is placed and the stock is updated, the frontend will show the order status.

Blazor Component: Place Order

In your Blazor frontend, you can create a component to place an order.

This simple Blazor component:

  1. Takes the product ID and quantity from the user.
  2. Sends a gRPC request to OrderService to place the order.
  3. Displays the status returned by OrderService (e.g., “Order created” or “Insufficient stock”).
@page "/place-order"

@inject GrpcChannel GrpcChannel
@inject NavigationManager Navigation

<h3>Place Order</h3>

<label for="productId">Product ID</label>
<input id="productId" @bind="productId" type="number" />
<label for="quantity">Quantity</label>
<input id="quantity" @bind="quantity" type="number" />

<button @onclick="PlaceOrder">Place Order</button>

<p>@status</p>

@code {
private int productId;
private int quantity;
private string status;

private async Task PlaceOrder()
{
var client = new OrderService.OrderServiceClient(GrpcChannel);
var request = new OrderRequest
{
ProductId = productId,
Quantity = quantity
};

var response = await client.PlaceOrderAsync(request);

status = response.Status;
}
}

Blazor Component: Display Products

You can create a component to display products by calling the ProductService API.

In this component, Blazor calls the ProductService API (GET /api/products) to get the list of products. You can display them in a list on the UI.

@page "/products"

@inject HttpClient Http

<h3>Products</h3>

<ul>
@foreach (var product in products)
{
<li>@product.Name - @product.Price</li>
}
</ul>

@code {
private List<Product> products;

protected override async Task OnInitializedAsync()
{
products = await Http.GetFromJsonAsync<List<Product>>("http://localhost:5000/api/products");
}

public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
}

Containerization

Containerization is crucial for ensuring that the microservices run consistently across different environments. In this architecture, each service (including the Blazor frontend) will be packaged as a Docker container.

Each microservice includes:

  • Dockerfile that specifies how the container is built.
  • Dapr sidecar (running alongside the service) to handle Pub/Sub, state management, and service invocation.

Example Docker File for OrderService

# Use the official .NET image as the base image
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

# Use the SDK image to build the app
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["OrderService/OrderService.csproj", "OrderService/"]
RUN dotnet restore "OrderService/OrderService.csproj"
COPY . .
WORKDIR "/src/OrderService"
RUN dotnet build "OrderService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "OrderService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderService.dll"]

Deploying with Dapr and Azure

After building the services and container images, you can deploy the microservices on Azure Kubernetes Service (AKS) or Azure Container Apps. Dapr will run as a sidecar in each container, providing all the necessary microservice patterns for service invocation, state management, and pub/sub communication.

Example Dapr Pub/Sub component (using Redis)

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
metadata:
- name: redisHost
value: "<REDIS_HOST>"
- name: redisPassword
value: "<REDIS_PASSWORD>"

This system illustrates how microservices can work together with gRPC for fast communication and Dapr for event-driven interactions. The Blazor frontend allows users to interact with these services seamlessly, placing orders, checking stock, and viewing product details.

Each service operates independently, allowing for flexible scaling, deployment, and maintenance while keeping the overall architecture clean and loosely coupled.