This setup provides a comprehensive structure for a .NET Core 8 API project using the CQRS pattern with MediatR, FluentValidation, JWT authentication, Swagger documentation, and unit/integration tests.
---
title: .NET API Clean Architecture Project using CQRS
subtitle: This setup provides a comprehensive structure for a .NET Core 8 API project using the CQRS pattern with MediatR, FluentValidation, JWT authentication, Swagger documentation, and unit/integration tests.
author: Jon LaBelle
date: September 28, 2024
source: https://jonlabelle.com/snippets/view/markdown/net-api-clean-architecture-project-using-cqrs
notoc: false
---
In this article, we will walk through the process of setting up a .NET Core 8
API project using the CQRS pattern with MediatR, FluentValidation, JWT
authentication, Swagger documentation, and unit/integration tests. We will use a
vertical slice architecture to keep our code organized and maintainable.
Additionally, we will create commands for all CRUD operations.
## Project Structure
Here is the structure of our project:
```plaintext
MyApi
├── MyApi.Api
│ ├── Controllers
│ │ └── WeatherForecastController.cs
│ ├── Program.cs
│ └── appsettings.json
├── MyApi.Application
│ ├── Commands
│ │ └── WeatherForecasts
│ │ ├── CreateWeatherForecastCommand.cs
│ │ ├── CreateWeatherForecastCommandHandler.cs
│ │ ├── CreateWeatherForecastCommandValidator.cs
│ │ ├── UpdateWeatherForecastCommand.cs
│ │ ├── UpdateWeatherForecastCommandHandler.cs
│ │ ├── UpdateWeatherForecastCommandValidator.cs
│ │ ├── DeleteWeatherForecastCommand.cs
│ │ ├── DeleteWeatherForecastCommandHandler.cs
│ │ └── DeleteWeatherForecastCommandValidator.cs
│ ├── Queries
│ │ └── WeatherForecasts
│ │ ├── GetWeatherForecastQuery.cs
│ │ ├── GetWeatherForecastQueryHandler.cs
│ │ └── GetWeatherForecastQueryValidator.cs
│ ├── Common
│ │ ├── Behaviors
│ │ │ └── ValidationBehavior.cs
│ │ └── Results
│ │ ├── Result.cs
│ │ └── ResultExtensions.cs
├── MyApi.Domain
│ └── Entities
│ └── WeatherForecast.cs
├── MyApi.Infrastructure
│ ├── Data
│ │ ├── ApplicationDbContext.cs
│ │ └── WeatherForecastRepository.cs
│ └── Services
│ └── JwtService.cs
├── MyApi.Tests
│ ├── UnitTests
│ │ └── WeatherForecastTests.cs
│ └── IntegrationTests
│ └── WeatherForecastIntegrationTests.cs
```
## Setting Up the Project
### 1. Create the Solution and Projects
```bash
dotnet new sln -n MyApi
dotnet new webapi -n MyApi.Api
dotnet new classlib -n MyApi.Application
dotnet new classlib -n MyApi.Domain
dotnet new classlib -n MyApi.Infrastructure
dotnet new xunit -n MyApi.Tests
dotnet sln add MyApi.Api/MyApi.Api.csproj
dotnet sln add MyApi.Application/MyApi.Application.csproj
dotnet sln add MyApi.Domain/MyApi.Domain.csproj
dotnet sln add MyApi.Infrastructure/MyApi.Infrastructure.csproj
dotnet sln add MyApi.Tests/MyApi.Tests.csproj
dotnet add MyApi.Api/MyApi.Api.csproj reference MyApi.Application/MyApi.Application.csproj
dotnet add MyApi.Api/MyApi.Api.csproj reference MyApi.Infrastructure/MyApi.Infrastructure.csproj
dotnet add MyApi.Application/MyApi.Application.csproj reference MyApi.Domain/MyApi.Domain.csproj
dotnet add MyApi.Infrastructure/MyApi.Infrastructure.csproj reference MyApi.Domain/MyApi.Domain.csproj
```
### 2. Install Required NuGet Packages
```bash
dotnet add package MediatR
dotnet add package FluentValidation
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
```
### 3. Configure Program.cs
```csharp
// MyApi.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMediatR(typeof(Program));
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<IWeatherForecastRepository, WeatherForecastRepository>();
builder.Services.AddTransient<IJwtService, JwtService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
```
### 4. Create the WeatherForecast Entity
```csharp
// MyApi.Domain/Entities/WeatherForecast.cs
namespace MyApi.Domain.Entities
{
public class WeatherForecast
{
public int Id { get; set; }
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
}
}
```
### 5. Create the ApplicationDbContext
```csharp
// MyApi.Infrastructure/Data/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApi.Domain.Entities;
namespace MyApi.Infrastructure.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<WeatherForecast> WeatherForecasts { get; set; }
}
}
```
### 6. Create the Repository
```csharp
// MyApi.Infrastructure/Data/WeatherForecastRepository.cs
using MyApi.Domain.Entities;
namespace MyApi.Infrastructure.Data
{
public interface IWeatherForecastRepository
{
Task<WeatherForecast> GetByIdAsync(int id);
Task AddAsync(WeatherForecast weatherForecast);
Task UpdateAsync(WeatherForecast weatherForecast);
Task DeleteAsync(int id);
}
public class WeatherForecastRepository : IWeatherForecastRepository
{
private readonly ApplicationDbContext _context;
public WeatherForecastRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<WeatherForecast> GetByIdAsync(int id)
{
return await _context.WeatherForecasts.FindAsync(id);
}
public async Task AddAsync(WeatherForecast weatherForecast)
{
await _context.WeatherForecasts.AddAsync(weatherForecast);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(WeatherForecast weatherForecast)
{
_context.WeatherForecasts.Update(weatherForecast);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var weatherForecast = await _context.WeatherForecasts.FindAsync(id);
if (weatherForecast != null)
{
_context.WeatherForecasts.Remove(weatherForecast);
await _context.SaveChangesAsync();
}
}
}
}
```
### 7. Create the Command and Query Handlers
#### Create Command
```csharp
// MyApi.Application/Commands/WeatherForecasts/CreateWeatherForecastCommand.cs
using MediatR;
using MyApi.Application.Common.Results;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class CreateWeatherForecastCommand : IRequest<Result<int>>
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
}
}
// MyApi.Application/Commands/WeatherForecasts/CreateWeatherForecastCommandHandler.cs
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Domain.Entities;
using MyApi.Infrastructure.Data;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class CreateWeatherForecastCommandHandler : IRequestHandler<CreateWeatherForecastCommand, Result<int>>
{
private readonly IWeatherForecastRepository _repository;
public CreateWeatherForecastCommandHandler(IWeatherForecastRepository repository)
{
_repository = repository;
}
public async Task<Result<int>> Handle(CreateWeatherForecastCommand request, CancellationToken cancellationToken)
{
var weatherForecast = new WeatherForecast
{
Date = request.Date,
TemperatureC = request.TemperatureC,
Summary = request.Summary
};
await _repository.AddAsync(weatherForecast);
return Result.Success(weatherForecast.Id);
}
}
}
// MyApi.Application/Commands/WeatherForecasts/CreateWeatherForecastCommandValidator.cs
using FluentValidation;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class CreateWeatherForecastCommandValidator : AbstractValidator<CreateWeatherForecastCommand>
{
public CreateWeatherForecastCommandValidator()
{
RuleFor(x => x.Date).NotEmpty();
RuleFor(x => x.TemperatureC).NotEmpty();
RuleFor(x => x.Summary).NotEmpty().MaximumLength(200);
}
}
```
#### Update Command
```csharp
// MyApi.Application/Commands/WeatherForecasts/UpdateWeatherForecastCommand.cs
using MediatR;
using MyApi.Application.Common.Results;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class UpdateWeatherForecastCommand : IRequest<Result>
{
public int Id { get; set; }
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
}
}
// MyApi.Application/Commands/WeatherForecasts/UpdateWeatherForecastCommandHandler.cs
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Domain.Entities;
using MyApi.Infrastructure.Data;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class UpdateWeatherForecastCommandHandler : IRequestHandler<UpdateWeatherForecastCommand, Result>
{
private readonly IWeatherForecastRepository _repository;
public UpdateWeatherForecastCommandHandler(IWeatherForecastRepository repository)
{
_repository = repository;
}
public async Task<Result> Handle(UpdateWeatherForecastCommand request, CancellationToken cancellationToken)
{
var weatherForecast = await _repository.GetByIdAsync(request.Id);
if (weatherForecast == null)
{
return Result.Failure("Weather forecast not found.");
}
weatherForecast.Date = request.Date;
weatherForecast.TemperatureC = request.TemperatureC;
weatherForecast.Summary = request.Summary;
await _repository.UpdateAsync(weatherForecast);
return Result.Success();
}
}
}
// MyApi.Application/Commands/WeatherForecasts/UpdateWeatherForecastCommandValidator.cs
using FluentValidation;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class UpdateWeatherForecastCommandValidator : AbstractValidator<UpdateWeatherForecastCommand>
{
public UpdateWeatherForecastCommandValidator()
{
RuleFor(x => x.Id).GreaterThan(0);
RuleFor(x => x.Date).NotEmpty();
RuleFor(x => x.TemperatureC).NotEmpty();
RuleFor(x => x.Summary).NotEmpty().MaximumLength(200);
}
}
```
#### Delete Command
```csharp
// MyApi.Application/Commands/WeatherForecasts/DeleteWeatherForecastCommand.cs
using MediatR;
using MyApi.Application.Common.Results;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class DeleteWeatherForecastCommand : IRequest<Result>
{
public int Id { get; set; }
}
}
// MyApi.Application/Commands/WeatherForecasts/DeleteWeatherForecastCommandHandler.cs
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Infrastructure.Data;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class DeleteWeatherForecastCommandHandler : IRequestHandler<DeleteWeatherForecastCommand, Result>
{
private readonly IWeatherForecastRepository _repository;
public DeleteWeatherForecastCommandHandler(IWeatherForecastRepository repository)
{
_repository = repository;
}
public async Task<Result> Handle(DeleteWeatherForecastCommand request, CancellationToken cancellationToken)
{
var weatherForecast = await _repository.GetByIdAsync(request.Id);
if (weatherForecast == null)
{
return Result.Failure("Weather forecast not found.");
}
await _repository.DeleteAsync(request.Id);
return Result.Success();
}
}
}
// MyApi.Application/Commands/WeatherForecasts/DeleteWeatherForecastCommandValidator.cs
using FluentValidation;
namespace MyApi.Application.Commands.WeatherForecasts
{
public class DeleteWeatherForecastCommandValidator : AbstractValidator<DeleteWeatherForecastCommand>
{
public DeleteWeatherForecastCommandValidator()
{
RuleFor(x => x.Id).GreaterThan(0);
}
}
```
#### Get Query
```csharp
// MyApi.Application/Queries/WeatherForecasts/GetWeatherForecastQuery.cs
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Domain.Entities;
namespace MyApi.Application.Queries.WeatherForecasts
{
public class GetWeatherForecastQuery : IRequest<Result<WeatherForecast>>
{
public int Id { get; set; }
}
}
// MyApi.Application/Queries/WeatherForecasts/GetWeatherForecastQueryHandler.cs
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Domain.Entities;
using MyApi.Infrastructure.Data;
namespace MyApi.Application.Queries.WeatherForecasts
{
public class GetWeatherForecastQueryHandler : IRequestHandler<GetWeatherForecastQuery, Result<WeatherForecast>>
{
private readonly IWeatherForecastRepository _repository;
public GetWeatherForecastQueryHandler(IWeatherForecastRepository repository)
{
_repository = repository;
}
public async Task<Result<WeatherForecast>> Handle(GetWeatherForecastQuery request, CancellationToken cancellationToken)
{
var weatherForecast = await _repository.GetByIdAsync(request.Id);
if (weatherForecast == null)
{
return Result.Failure<WeatherForecast>("Weather forecast not found.");
}
return Result.Success(weatherForecast);
}
}
}
// MyApi.Application/Queries/WeatherForecasts/GetWeatherForecastQueryValidator.cs
using FluentValidation;
namespace MyApi.Application.Queries.WeatherForecasts
{
public class GetWeatherForecastQueryValidator : AbstractValidator<GetWeatherForecastQuery>
{
public GetWeatherForecastQueryValidator()
{
RuleFor(x => x.Id).GreaterThan(0);
}
}
```
### 8. Create Result Pattern Classes
```csharp
// MyApi.Application/Common/Results/Result.cs
namespace MyApi.Application.Common.Results
{
public class Result
{
public bool IsSuccess { get; }
public string Error { get; }
protected Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new Result(true, string.Empty);
public static Result Failure(string error) => new Result(false, error);
}
public class Result<T> : Result
{
public T Value { get; }
protected Result(bool isSuccess, string error, T value) : base(isSuccess, error)
{
Value = value;
}
public static Result<T> Success(T value) => new Result<T>(true, string.Empty, value);
public static Result<T> Failure(string error) => new Result<T>(false, error, default);
}
}
```
### 9. Create the Controller
```csharp
// MyApi.Api/Controllers/WeatherForecastController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using MyApi.Application.Commands.WeatherForecasts;
using MyApi.Application.Queries.WeatherForecasts;
namespace MyApi.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMediator _mediator;
public WeatherForecastController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(CreateWeatherForecastCommand command)
{
var result = await _mediator.Send(command);
if (!result.IsSuccess)
{
return BadRequest(result.Error);
}
return Ok(result.Value);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateWeatherForecastCommand command)
{
if (id != command.Id)
{
return BadRequest("ID mismatch");
}
var result = await _mediator.Send(command);
if (!result.IsSuccess)
{
return BadRequest(result.Error);
}
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var command = new DeleteWeatherForecastCommand { Id = id };
var result = await _mediator.Send(command);
if (!result.IsSuccess)
{
return NotFound(result.Error);
}
return NoContent();
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var query = new GetWeatherForecastQuery { Id = id };
var result = await _mediator.Send(query);
if (!result.IsSuccess)
{
return NotFound(result.Error);
}
return Ok(result.Value);
}
}
}
```
### 10. Setup exception handling
Set up a global exception handler that follows the Problem Details specification
and uses a base exception class with status/error codes.
#### 1. Create a Base Exception Class
Create a base exception class that includes a status code and an error code.
**File: MyApi.Domain/Exceptions/AppException.cs**
```csharp
using System;
public abstract class AppException : Exception
{
public int StatusCode { get; }
public string ErrorCode { get; }
protected AppException(string message, int statusCode, string errorCode) : base(message)
{
StatusCode = statusCode;
ErrorCode = errorCode;
}
}
```
#### 2. Create Specific Exception Classes
Create specific exception classes that inherit from the base exception class.
**File: MyApi.Domain/Exceptions/NotFoundException.cs**
```csharp
public class NotFoundException : AppException
{
public NotFoundException(string message) : base(message, 404, "NotFound") { }
}
```
**File: MyApi.Domain/Exceptions/ValidationException.cs**
```csharp
public class ValidationException : AppException
{
public ValidationException(string message) : base(message, 400, "ValidationError") { }
}
```
#### 3. Create a Global Exception Handling Middleware
Create a middleware to handle exceptions globally and format the response according to the Problem Details specification.
**File: MyApi.Api/Middleware/ExceptionHandlingMiddleware.cs**
```csharp
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception has occurred.");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/problem+json";
var statusCode = (int)HttpStatusCode.InternalServerError;
var errorCode = "InternalServerError";
var detail = "An unexpected error occurred. Please try again later.";
if (exception is AppException appException)
{
statusCode = appException.StatusCode;
errorCode = appException.ErrorCode;
detail = appException.Message;
}
context.Response.StatusCode = statusCode;
var problemDetails = new
{
type = $"https://httpstatuses.com/{statusCode}",
title = errorCode,
status = statusCode,
detail = detail,
instance = context.Request.Path
};
var json = JsonSerializer.Serialize(problemDetails);
return context.Response.WriteAsync(json);
}
}
```
#### 4. Register the Middleware in Program.cs
Register the middleware in your `Program.cs` file.
**File: MyApi.Api/Program.cs**
```csharp
// Add this line after "var app = builder.Build()"
app.UseMiddleware<ExceptionHandlingMiddleware>();
```
#### 5. Use the Custom Exceptions in Your Application
Throw the custom exceptions in your application where appropriate.
**File: MyApi.Application/Commands/CreateWeatherForecast/CreateWeatherForecastCommandHandler.cs**
```csharp
using MediatR;
using MyApi.Application.Common.Results;
using MyApi.Domain.Entities;
using MyApi.Infrastructure.Data;
using System;
using System.Threading;
using System.Threading.Tasks;
public class CreateWeatherForecastCommandHandler : IRequestHandler<CreateWeatherForecastCommand, Result<int>>
{
private readonly IWeatherForecastRepository _repository;
public CreateWeatherForecastCommandHandler(IWeatherForecastRepository repository)
{
_repository = repository;
}
public async Task<Result<int>> Handle(CreateWeatherForecastCommand request, CancellationToken cancellationToken)
{
try
{
var weatherForecast = new WeatherForecast
{
Date = request.Date,
TemperatureC = request.TemperatureC,
Summary = request.Summary
};
await _repository.AddAsync(weatherForecast);
return Result.Success(weatherForecast.Id);
}
catch (Exception ex)
{
// Log the exception and return a failure result
throw new ValidationException("An error occurred while creating the weather forecast.");
}
}
}
```
**File: MyApi.Api/Controllers/WeatherForecastController.cs**
```csharp
using MediatR;
using Microsoft.AspNetCore.Mvc;
using MyApi.Application.Commands.CreateWeatherForecast;
using MyApi.Application.Queries.GetWeatherForecast;
using MyApi.Domain.Exceptions;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMediator _mediator;
public WeatherForecastController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Create(CreateWeatherForecastCommand command)
{
var result = await _mediator.Send(command);
if (!result.IsSuccess)
{
return BadRequest(result.Error);
}
return Ok(result.Value);
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
try
{
var query = new GetWeatherForecastQuery { Id = id };
var result = await _mediator.Send(query);
if (!result.IsSuccess)
{
throw new NotFoundException("Weather forecast not found.");
}
return Ok(result.Value);
}
catch (NotFoundException ex)
{
return NotFound(ex.Message);
}
}
}
```
### 11. Add Unit and Integration Tests
#### Unit Tests
Unit tests will focus on testing individual components in isolation. We will use
xUnit and Moq for our unit tests.
```bash
dotnet add MyApi.Tests package xunit
dotnet add MyApi.Tests package Moq
```
**File: MyApi.Tests/UnitTests/WeatherForecastTests.cs**
```csharp
using Xunit;
using Moq;
using MyApi.Application.Commands.CreateWeatherForecast;
using MyApi.Infrastructure.Data;
using MyApi.Domain.Entities;
using System.Threading;
using System.Threading.Tasks;
public class WeatherForecastTests
{
[Fact]
public async Task CreateWeatherForecast_ShouldReturnSuccess()
{
// Arrange
var repositoryMock = new Mock<IWeatherForecastRepository>();
var handler = new CreateWeatherForecastCommandHandler(repositoryMock.Object);
var command = new CreateWeatherForecastCommand
{
Date = DateTime.Now,
TemperatureC = 25,
Summary = "Sunny"
};
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
Assert.True(result.IsSuccess);
repositoryMock.Verify(r => r.AddAsync(It.IsAny<WeatherForecast>()), Times.Once);
}
[Fact]
public async Task GetWeatherForecast_ShouldReturnSuccess()
{
// Arrange
var repositoryMock = new Mock<IWeatherForecastRepository>();
var handler = new GetWeatherForecastQueryHandler(repositoryMock.Object);
var query = new GetWeatherForecastQuery { Id = 1 };
var weatherForecast = new WeatherForecast { Id = 1, Date = DateTime.Now, TemperatureC = 25, Summary = "Sunny" };
repositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync(weatherForecast);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
Assert.True(result.IsSuccess);
Assert.Equal(weatherForecast, result.Value);
}
}
```
#### Integration Tests
Integration tests will focus on testing the interaction between multiple
components. We will use xUnit and the `WebApplicationFactory` class for our
integration tests.
```bash
dotnet add MyApi.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet add MyApi.Tests package xunit
dotnet add MyApi.Tests package Moq
dotnet add MyApi.Tests package Microsoft.EntityFrameworkCore.InMemory
```
First, create a base class that handles the common setup tasks such as
configuring the test server and seeding the database.
**File: MyApi.Tests/IntegrationTests/IntegrationTestBase.cs**
```csharp
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MyApi.Api;
using MyApi.Infrastructure.Data;
using System;
using System.Linq;
using System.Net.Http;
using Xunit;
public class IntegrationTestBase : IClassFixture<WebApplicationFactory<Program>>
{
protected readonly HttpClient Client;
private readonly WebApplicationFactory<Program> _factory;
public IntegrationTestBase(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(descriptor);
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
db.Database.EnsureCreated();
SeedDatabase(db);
}
});
});
Client = _factory.CreateClient();
}
private void SeedDatabase(ApplicationDbContext db)
{
db.WeatherForecasts.Add(new MyApi.Domain.Entities.WeatherForecast
{
Date = DateTime.Now,
TemperatureC = 25,
Summary = "Sunny"
});
db.SaveChanges();
}
}
```
**File: MyApi.Tests/IntegrationTests/WeatherForecastIntegrationTests.cs**
```csharp
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using MyApi.Application.Commands.CreateWeatherForecast;
using Xunit;
public class WeatherForecastIntegrationTests : IntegrationTestBase
{
public WeatherForecastIntegrationTests(WebApplicationFactory<Program> factory) : base(factory) { }
[Fact]
public async Task CreateWeatherForecast_ShouldReturnSuccess()
{
// Arrange
var command = new CreateWeatherForecastCommand
{
Date = DateTime.Now,
TemperatureC = 25,
Summary = "Sunny"
};
// Act
var response = await Client.PostAsJsonAsync("/api/weatherforecast", command);
// Assert
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task GetWeatherForecast_ShouldReturnSuccess()
{
// Act
var response = await Client.GetAsync("/api/weatherforecast/1");
// Assert
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task GetWeatherForecast_ShouldReturnNotFound()
{
// Act
var response = await Client.GetAsync("/api/weatherforecast/999");
// Assert
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
}
}
```
You can now easily add more integration tests by creating new test methods in
the `WeatherForecastIntegrationTests` class or by creating new test classes that
inherit from `IntegrationTestBase`.
**Example: Additional Integration Test**
```csharp
public class AdditionalWeatherForecastIntegrationTests : IntegrationTestBase
{
public AdditionalWeatherForecastIntegrationTests(WebApplicationFactory<Program> factory) : base(factory) { }
[Fact]
public async Task UpdateWeatherForecast_ShouldReturnSuccess()
{
// Arrange
var updateCommand = new UpdateWeatherForecastCommand
{
Id = 1,
Date = DateTime.Now.AddDays(1),
TemperatureC = 30,
Summary = "Hot"
};
// Act
var response = await Client.PutAsJsonAsync("/api/weatherforecast/1", updateCommand);
// Assert
response.EnsureSuccessStatusCode();
}
}
```
#### Run Your Tests
You can run your tests using the following command:
```bash
dotnet test
```
This will execute all the tests in your test project, including the integration tests.