Skip to content

Commit 7ea6853

Browse files
Clean Architecture: replace the ToDoApp sample with EventTicketing on .NET 10 (#2238)
The article is rewritten around a small ticket-reservation API: Domain, Application, Infrastructure and Api projects, the Result pattern mapped to ProblemDetails, plain use-case handlers (no MediatR), EF Core with SQLite, unit tests with fakes, and NetArchTest architecture tests that enforce the Dependency Rule. Targets net10.0. Build: 0 warnings. Tests: 6 unit, 3 architecture, all passing.
1 parent 374559c commit 7ea6853

56 files changed

Lines changed: 793 additions & 433 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace EventTicketing.Api.Contracts;
4+
5+
public sealed record ReserveTicketsRequest([property: Range(1, 20)] int Quantity);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using EventTicketing.Api.Contracts;
2+
using EventTicketing.Api.Extensions;
3+
using EventTicketing.Application.Events;
4+
5+
namespace EventTicketing.Api.Endpoints;
6+
7+
public static class EventEndpoints
8+
{
9+
public static IEndpointRouteBuilder MapEventEndpoints(this IEndpointRouteBuilder app)
10+
{
11+
var events = app.MapGroup("/api/events");
12+
13+
events.MapGet("/{eventId:int}", async (
14+
int eventId,
15+
GetEventAvailabilityHandler handler,
16+
CancellationToken cancellationToken) =>
17+
{
18+
var result = await handler.HandleAsync(new GetEventAvailabilityQuery(eventId), cancellationToken);
19+
20+
return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem();
21+
});
22+
23+
events.MapPost("/{eventId:int}/reservations", async (
24+
int eventId,
25+
ReserveTicketsRequest request,
26+
ReserveTicketsHandler handler,
27+
CancellationToken cancellationToken) =>
28+
{
29+
var command = new ReserveTicketsCommand(eventId, request.Quantity);
30+
var result = await handler.HandleAsync(command, cancellationToken);
31+
32+
return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem();
33+
});
34+
35+
return app;
36+
}
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\EventTicketing.Application\EventTicketing.Application.csproj" />
5+
<ProjectReference Include="..\EventTicketing.Infrastructure\EventTicketing.Infrastructure.csproj" />
6+
</ItemGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
10+
</ItemGroup>
11+
12+
<PropertyGroup>
13+
<TargetFramework>net10.0</TargetFramework>
14+
<Nullable>enable</Nullable>
15+
<ImplicitUsings>enable</ImplicitUsings>
16+
</PropertyGroup>
17+
18+
</Project>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using EventTicketing.Domain.Common;
2+
3+
namespace EventTicketing.Api.Extensions;
4+
5+
public static class ResultExtensions
6+
{
7+
public static IResult ToProblem(this Result result)
8+
{
9+
if (result.IsSuccess)
10+
throw new InvalidOperationException("A successful result is not a problem.");
11+
12+
var statusCode = result.Error.Type switch
13+
{
14+
ErrorType.Validation => StatusCodes.Status400BadRequest,
15+
ErrorType.NotFound => StatusCodes.Status404NotFound,
16+
ErrorType.Conflict => StatusCodes.Status409Conflict,
17+
_ => StatusCodes.Status500InternalServerError
18+
};
19+
20+
return TypedResults.Problem(
21+
statusCode: statusCode,
22+
title: result.Error.Code,
23+
detail: result.Error.Description);
24+
}
25+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using EventTicketing.Api.Endpoints;
2+
using EventTicketing.Application;
3+
using EventTicketing.Infrastructure;
4+
using EventTicketing.Infrastructure.Persistence;
5+
6+
var builder = WebApplication.CreateBuilder(args);
7+
8+
builder.Services.AddApplication();
9+
builder.Services.AddInfrastructure(builder.Configuration.GetConnectionString("Ticketing")!);
10+
11+
builder.Services.AddProblemDetails();
12+
builder.Services.AddValidation();
13+
builder.Services.AddOpenApi();
14+
15+
var app = builder.Build();
16+
17+
app.UseExceptionHandler();
18+
19+
if (app.Environment.IsDevelopment())
20+
{
21+
app.MapOpenApi();
22+
await app.Services.SeedDatabaseAsync();
23+
}
24+
25+
app.MapEventEndpoints();
26+
27+
app.Run();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": false,
8+
"applicationUrl": "http://localhost:5000",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
}
13+
}
14+
}

csharp-architectural-patterns/CleanArchitecture/ToDoApp.API/appsettings.json renamed to csharp-architectural-patterns/CleanArchitecture/EventTicketing.Api/appsettings.Development.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
"Default": "Information",
55
"Microsoft.AspNetCore": "Warning"
66
}
7-
},
8-
"AllowedHosts": "*"
7+
}
98
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"ConnectionStrings": {
3+
"Ticketing": "Data Source=tickets.db"
4+
},
5+
"Logging": {
6+
"LogLevel": {
7+
"Default": "Information",
8+
"Microsoft.AspNetCore": "Warning"
9+
}
10+
},
11+
"AllowedHosts": "*"
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace EventTicketing.Application.Abstractions;
2+
3+
public interface IUnitOfWork
4+
{
5+
Task SaveChangesAsync(CancellationToken cancellationToken = default);
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using EventTicketing.Application.Events;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace EventTicketing.Application;
5+
6+
public static class DependencyInjection
7+
{
8+
public static IServiceCollection AddApplication(this IServiceCollection services)
9+
{
10+
services.AddScoped<ReserveTicketsHandler>();
11+
services.AddScoped<GetEventAvailabilityHandler>();
12+
13+
return services;
14+
}
15+
}

0 commit comments

Comments
 (0)