Building a Dapr-based Microservices Architecture in C#

Summary

This tutorial walks you through creating a microservices-based architecture using Dapr and C#. We’ll build 5 microservices that work together to process a customer form submission. Dapr will handle service invocation, state management, and pub/sub communication.

Prerequisites

Before getting started, ensure you have the following installed:

– Dapr installed on your local machine (Dapr installation guide)
– .NET Core SDK (Download .NET Core)
– Docker installed

Step 1: Create the Solution and Projects

First, create a new solution and 5 microservices as individual .NET Web API projects:

# Create a solution mkdir dapr-csharp-sample cd dapr-csharp-sample dotnet new sln --name DaprCSharpSample # Create each microservice project dotnet new webapi -n FrontendService dotnet new webapi -n ValidationService dotnet new webapi -n CustomerService dotnet new webapi -n NotificationService dotnet new webapi -n SalesService # Add projects to the solution dotnet sln add FrontendService/FrontendService.csproj dotnet sln add ValidationService/ValidationService.csproj dotnet sln add CustomerService/CustomerService.csproj dotnet sln add NotificationService/NotificationService.csproj dotnet sln add SalesService/SalesService.csproj 

Step 2: Frontend Service

The Frontend Service receives the form data and calls the Validation Service via Dapr’s service invocation.

using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; [ApiController] [Route("[controller]")] public class FormController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public FormController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [HttpPost("submit")] public async Task SubmitForm([FromBody] FormData data) { var httpClient = _httpClientFactory.CreateClient(); var jsonContent = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); // Call Validation Service via Dapr Service Invocation var response = await httpClient.PostAsync("http://localhost:3500/v1.0/invoke/validation-service/method/validate", jsonContent); var result = await response.Content.ReadAsStringAsync(); return Ok(result); } } public class FormData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }

Modify Startup.cs to add HttpClient:

public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddHttpClient(); // Add HttpClient for service invocation }

Run the service:

dapr run --app-id frontend-service --app-port 5000 -- dotnet run --project FrontendService/FrontendService.csproj

Step 3: Validation Service

The Validation Service validates the form data and calls the Customer Service to store the data.

using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; [ApiController] [Route("[controller]")] public class ValidationController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public ValidationController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [HttpPost("validate")] public async Task ValidateForm([FromBody] FormData data) { if (string.IsNullOrEmpty(data.Email) || string.IsNullOrEmpty(data.Name)) { return BadRequest("Validation failed"); } var httpClient = _httpClientFactory.CreateClient(); var jsonContent = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); // Call Customer Service to store data after validation var response = await httpClient.PostAsync("http://localhost:3500/v1.0/invoke/customer-service/method/store", jsonContent); var result = await response.Content.ReadAsStringAsync(); return Ok(result); } } public class FormData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }

Run the service:

dapr run --app-id validation-service --app-port 6000 -- dotnet run --project ValidationService/ValidationService.csproj

Step 4: Customer Service (State Management)

The Customer Service stores the customer’s data using Dapr’s state management API and publishes an event using the pub/sub API.

using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; [ApiController] [Route("[controller]")] public class CustomerController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public CustomerController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [HttpPost("store")] public async Task StoreCustomer([FromBody] FormData data) { var stateUrl = "http://localhost:3500/v1.0/state/statestore"; var pubsubUrl = "http://localhost:3500/v1.0/publish/pubsub/newCustomer"; var httpClient = _httpClientFactory.CreateClient(); var stateContent = new StringContent(JsonConvert.SerializeObject(new[] { new { key = $"customer_{data.Email}", value = data } }), Encoding.UTF8, "application/json"); // Store customer data in the state store await httpClient.PostAsync(stateUrl, stateContent); // Publish event to notify other services var pubContent = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); await httpClient.PostAsync(pubsubUrl, pubContent); return Ok($"Customer data stored and event published for {data.Name}"); } } public class FormData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }

Run the service:

dapr run --app-id customer-service --app-port 7000 -- dotnet run --project CustomerService/CustomerService.csproj

Step 5: Notification Service (Pub/Sub and Bindings)

The Notification Service listens for the event and sends a confirmation email using Dapr’s output bindings (e.g., SendGrid).

using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; [ApiController] [Route("[controller]")] public class NotificationController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public NotificationController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [HttpPost("newCustomer")] public async Task SendEmail([FromBody] FormData data) { var sendgridUrl = "http://localhost:3500/v1.0/bindings/sendgrid"; var message = new { to = data.Email, subject = "Thank you for your inquiry", text = $"Hello {data.Name}, we received your message: {data.Message}. We will contact you soon." }; var httpClient = _httpClientFactory.CreateClient(); var emailContent = new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json"); await httpClient.PostAsync(sendgridUrl, emailContent); return Ok($"Email sent to {data.Email}"); } } public class FormData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }

Run the service:

dapr run --app-id notification-service --app-port 8000 -- dotnet run --project NotificationService/NotificationService.csproj

Step 6: Sales Service (Pub/Sub)

The Sales Service also listens for the event and notifies the sales team.

using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; [ApiController] [Route("[controller]")] public class SalesController : ControllerBase { [HttpPost("newCustomer")] public async Task NotifySales([FromBody] FormData data) { // Notify the sales team (for example, by sending a message to Slack) return Ok($"Sales team notified for {data.Name}"); } } public class FormData { public string Name { get; set; } public string Email { get; set; } public string Message { get; set; } }

Run the service:

dapr run --app-id sales-service --app-port 9000 -- dotnet run --project SalesService/SalesService.csproj

Step 7: Configure Dapr Components

Create the Dapr components to enable state management and pub/sub functionality. In the components/ folder, add the following YAML files:

State Store (Redis)

apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore namespace: default spec: type: state.redis version: v1 metadata: - name: redisHost value: "localhost:6379" - name: redisPassword value: ""

Pub/Sub (Redis)

apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: pubsub namespace: default spec: type: pubsub.redis version: v1 metadata: - name: redisHost value: "localhost:6379" - name: redisPassword value: ""

SendGrid Binding

apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: sendgrid namespace: default spec: type: bindings.sendgrid version: v1 metadata: - name: apiKey value: "YOUR_SENDGRID_API_KEY"

Step 8: Run Everything

To run the entire system, follow these steps:

1. Start Redis (for State Store and Pub/Sub)

Run Redis using Docker to handle state management and pub/sub:

docker run -d --name redis -p 6379:6379 redis

2. Start All Microservices

Start each service using the following command in separate terminal windows. Make sure you are in each service’s directory when running the command:

# Frontend Service dapr run --app-id frontend-service --app-port 5000 -- dotnet run --project FrontendService/FrontendService.csproj # Validation Service dapr run --app-id validation-service --app-port 6000 -- dotnet run --project ValidationService/ValidationService.csproj # Customer Service dapr run --app-id customer-service --app-port 7000 -- dotnet run --project CustomerService/CustomerService.csproj # Notification Service dapr run --app-id notification-service --app-port 8000 -- dotnet run --project NotificationService/NotificationService.csproj # Sales Service dapr run --app-id sales-service --app-port 9000 -- dotnet run --project SalesService/SalesService.csproj

3. Submit a Form

To test the system, you can use Postman or cURL to submit a form to the Frontend Service:

curl -X POST http://localhost:5000/form/submit \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com", "message": "Interested in product X"}'

Step 9: Verify the Flow

When the form is submitted, the following flow will occur:

– The Frontend Service invokes the Validation Service.
– The Validation Service calls the Customer Service to store the customer data.
– The Customer Service stores the data in Redis (state store) and publishes a message via Redis (pub/sub).
– The Notification Service listens for the message and sends a confirmation email via SendGrid.
– The Sales Service also listens for the event and notifies the sales team.

Conclusion

In this tutorial, we’ve built a microservices architecture using Dapr and C# with ASP.NET Core. Dapr handled critical cross-cutting concerns like service invocation, state management, pub/sub, and external bindings, allowing us to focus more on business logic.

You can further extend this setup by integrating more services or enhancing the existing ones, such as improving error handling, adding retries, or implementing circuit breakers with Dapr’s resiliency features.