MediatR CQRS Example with Azure Functions in C# (.NET 8, Clean Architecture)

An Azure Functions v4 solution on .NET 8 that demonstrates MediatR and CQRS (Command Query Responsibility Segregation) with a clean multi-layer 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.

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

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)

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 if handler execution exceeds 500 ms

Data Storage

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

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

Running Locally

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

Usage

# 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 both endpoints return 400 Bad Request for an invalid request.

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

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.

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