C# and .NET examples · Application architecture

MediatR and CQRS with Azure Functions in C#

An Azure Functions v4 solution on .NET 8 that demonstrates MediatR and CQRS (Command Query Responsibility Segregation) with a layered architecture and MediatR pipeline behaviours for logging and performance monitoring. The solution implements user creation and retrieval through CQRS commands and queries, wired with two MediatR pipeline behaviours.

In this guide: Get started · Walk through the example · Understand the details · Check the limits

✨ Features

Projects

Project Description
Mediat.AzureFunction Azure Functions host — HTTP triggers for create and get user
Mediat.Business CreateUserCommand + handler, GetUserQuery + handler
Mediat.Data IUserRepository interface and in-memory UserRepository
Mediat.Infrastructure LoggingBehavior and PerformanceMonitoringBehavior
Mediat.Model User entity (Id, Name, Email)

🚀 Quick start

Running Locally

cd Mediat/Mediat.AzureFunction
func start --port 7094

📦 Prerequisites and installation

Requirements

Minimum local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}

NuGet Packages (Mediat.AzureFunction)

Package Version
MediatR.Extensions.Microsoft.DependencyInjection 11.0.0
Microsoft.Azure.Functions.Worker 2.0.0
Microsoft.Azure.Functions.Worker.Extensions.Http 3.3.0
Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore 2.0.1
Microsoft.Azure.Functions.Worker.Sdk 2.0.2
Newtonsoft.Json 13.0.3

💡 Usage

HTTP Endpoints

Function Method Route Description
create-user-function POST /api/user Creates a new user from JSON body
get-user-function GET /api/user/{id} Retrieves a user by integer ID

Request Body (POST)

{
  "name": "Alice",
  "email": "alice@example.com"
}

Response (POST / GET)

{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com"
}

Pipeline Behaviours

Behaviour Description
LoggingBehavior<TRequest, TResponse> Logs Handling {request type} before and Handled {response type} after each handler call
PerformanceMonitoringBehavior<TRequest, TResponse> Logs a warning after a successful downstream return when elapsed time exceeds 500 ms; thrown failures bypass that warning

Data Storage

UserRepository uses a static in-memory List<User>. IDs are assigned sequentially (count + 1). Data is not persisted between application restarts.

# Create a user
curl -X POST http://localhost:7094/api/user \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

# Retrieve a user
curl http://localhost:7094/api/user/1

GET /api/user/{id} returns 404 Not Found when no user has that ID, and selected null-request/null-command paths return 400 Bad Request; malformed JSON and invalid field values are not comprehensively handled.

🏗️ Architecture

HTTP Request
     │
     ▼
Mediat.AzureFunction     ← Azure Functions host (HTTP triggers)
     │  sends IRequest via IMediator
     ▼
Mediat.Business          ← Command/Query handlers
     │  calls repository
     ▼
Mediat.Data              ← In-memory repository implementation
     │  uses model
     ▼
Mediat.Model             ← User entity

Pipeline behaviours (Mediat.Infrastructure) wrap every handler call:

LoggingBehavior → PerformanceMonitoringBehavior → Handler

Project Structure

Mediat/
├── Mediat.AzureFunction/
│   ├── CreateUserFunction.cs          # POST /api/user
│   ├── GetUserFunction.cs             # GET /api/user/{id}
│   └── Program.cs                     # MediatR and DI registration
├── Mediat.Business/
│   ├── Commands/UserCommand/
│   │   ├── CreateUserCommand.cs
│   │   └── CreateUserCommandHandler.cs
│   └── Queries/UserQuery/
│       ├── GetUserQuery.cs
│       └── GetUserQueryHandler.cs
├── Mediat.Data/
│   ├── Contract/IUserRepository.cs
│   └── Repository/UserRepository.cs   # In-memory implementation
├── Mediat.Infrastructure/
│   ├── LoggingBehavior.cs
│   └── PerformanceMonitoringBehavior.cs
├── Mediat.Model/
│   └── User.cs                        # Id, Name, Email
└── Mediat.sln

🧪 Development and testing

Check restore/build compatibility for Mediat/Mediat.sln with its x64 solution platform, then test create, get, missing ID and restart behavior in a local Functions host. The Release solution configuration lacks a Build.0 entry for Mediat.Infrastructure; inspect Configuration Manager and project-reference build behavior before treating a solution build as evidence for every project. Package references and handlers were inspected; restore, host execution and concurrent requests were not tested here.

🛠️ Troubleshooting and limitations

Notes

  • Mediat.AzureFunction references MediatR.Extensions.Microsoft.DependencyInjection 11.0.0, while Mediat.Business and Mediat.Infrastructure reference MediatR 12.4.1, which includes the AddMediatR registration itself. The separate DI extension package is not needed with MediatR 12 and later.
  • Pipeline behaviours run in registration order, so LoggingBehavior wraps PerformanceMonitoringBehavior, which wraps the handler.

The static List<User> and count-based ID allocation are unsynchronized and lose data on restart. Neither name nor email is validated. A null deserialization result gets 400, but malformed JSON exceptions are not caught and converted to 400. The host references MediatR DI extensions 11.0.0 while business/infrastructure use MediatR 12.4.1; package compatibility and restore require separate verification. Start Azurite for the development storage setting.


Read all C# and .NET Code Examples guides online · Project overview · All developer projects