Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore
run: dotnet restore SchoolAccount.ApiTemplate.slnx
run: dotnet restore SchoolAccount.CollectAPI.slnx

- name: Check formatting
run: |
dotnet tool restore
dotnet csharpier check .

- name: Build
run: dotnet build SchoolAccount.ApiTemplate.slnx --configuration Release --no-restore
run: dotnet build SchoolAccount.CollectAPI.slnx --configuration Release --no-restore

- name: Test
run: dotnet test SchoolAccount.ApiTemplate.slnx --configuration Release --no-restore --no-build -- --report-trx --coverage --coverage-output-format cobertura --coverage-settings ${{ github.workspace }}/coverage.config
run: dotnet test SchoolAccount.CollectAPI.slnx --configuration Release --no-restore --no-build -- --report-trx --coverage --coverage-output-format cobertura --coverage-settings ${{ github.workspace }}/coverage.config

- name: Test report
uses: EnricoMi/publish-unit-test-result-action@v2
Expand Down Expand Up @@ -69,4 +69,4 @@ jobs:
path: TestResults/Merged/SummaryGithub.md

- name: Publish
run: dotnet publish SchoolAccount.ApiTemplate.slnx --configuration Release --no-restore --no-build
run: dotnet publish SchoolAccount.CollectAPI.slnx --configuration Release --no-restore --no-build
1 change: 0 additions & 1 deletion .idea/.idea.SchoolAccount.ApiTemplate/.idea/.name

This file was deleted.

1 change: 1 addition & 0 deletions .idea/.idea.SchoolAccount.CollectAPI/.idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
<!-- Infrastructure -->
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.10" />
<!-- Web.Api -->
<!-- Collect.Api -->
Comment thread
markysoft marked this conversation as resolved.
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<!-- Transitive pin: Microsoft.AspNetCore.OpenApi brings in 2.0.0, which has GHSA-v5pm-xwqc-g5wc -->
<PackageVersion Include="Microsoft.OpenApi" Version="2.10.0" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.16.16" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.16.17" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Seq" Version="9.1.0" />
<!-- ArchitectureTests -->
Expand Down
42 changes: 22 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Introduction

SchoolAccount-ApiTemplate is a template for building ASP.NET Core Web APIs for the DfE School Account service. It
SchoolAccount-CollectAPI is an API for the DfE School Account service built on .NET 10 based on this [Api-Template](https://github.com/DFE-Digital/SchoolAccount-ApiTemplate). It
provides a minimal clean architecture solution, with CQRS abstractions, structured logging, error handling, and
architecture tests already wired up, so new backend services can start from a consistent, proven baseline rather than
from scratch.
architecture tests.

## Documentation

Expand Down Expand Up @@ -44,23 +43,26 @@ Follow these steps to start the API locally.

3. Run the API using one of the following:

| Method | Command | Outcome |
|----------------|--------------------------------------------------|----------------------------------------------------------------------|
| Docker Compose | `docker compose up --build` | Starts the API and its dependencies (Seq) in containers |
| .NET CLI | `dotnet run --project src/SchoolAccount.Web.Api` | Runs the API directly using the `http` launch profile, no containers |
| Method | Command | Outcome |
|----------------|------------------------------------------------------|----------------------------------------------------------------------|
| Docker Compose | `docker compose up --build` | Starts the API and its dependencies (Seq) in containers |
| .NET CLI | `dotnet run --project src/SchoolAccount.Collect.Api` | Runs the API directly using the `http` launch profile, no containers |

In Rider or Visual Studio you can use the equivalent `docker-compose` or `http` run configurations from the toolbar.

4. Once running, the API is available at `http://localhost:5100`:
- Interactive API reference (Scalar) at `http://localhost:5100/scalar/v1`
- Health checks at `http://localhost:5100/health`
**Note**: When running the application the messages `No action descriptors found.` may appear. This is a common message
for minimal APIs due to ASP.NET Core not being able to register controllers/endpoints.

4. Once running, the API is available at `http://localhost:5101`:
- Interactive API reference (Scalar) at `http://localhost:5101/scalar/v1`
- Health checks at `http://localhost:5101/health`
- Logs (if started with compose) at `http://localhost:8081`

> The Scalar API reference is only mapped in the `Development` environment.

5. Debugging guidance:
- Set breakpoints in your C# files under `src/` and start either run configuration with debugging enabled.
- `.http` files alongside the endpoints in `src/SchoolAccount.Web.Api/Endpoints` can be used to exercise the API from your IDE.
- `.http` files alongside the endpoints in `src/SchoolAccount.Collect.Api/Endpoints` can be used to exercise the API from your IDE.

# Build and Test

Expand All @@ -78,7 +80,7 @@ Use the .NET CLI to build or test the solution.
dotnet test
```

Architecture tests under `tests/SchoolAccount.ArchitectureTests` enforce the clean architecture dependency rules between layers.
Architecture tests under `tests/SchoolAccount.Collect.ArchitectureTests` enforce the clean architecture dependency rules between layers.

### Formatting

Expand Down Expand Up @@ -133,13 +135,13 @@ finishes:

The solution follows a clean architecture pattern with vertical slice features:

| Project | Purpose |
|--------------------------------|--------------------------------------------------------------|
| `SchoolAccount.Web.Api` | ASP.NET Core Web API - endpoints, middleware, error handling |
| `SchoolAccount.Application` | CQRS handlers and feature logic, organised by feature folder |
| `SchoolAccount.Domain` | Domain entities and business rules |
| `SchoolAccount.Infrastructure` | External concerns - time, data access, integrations |
| `SchoolAccount.SharedKernel` | Shared primitives - `Result<T>`, `Error`, `ValidationError` |
| Project | Purpose |
|----------------------------------------|--------------------------------------------------------------|
| `SchoolAccount.Collect.Api` | ASP.NET Core Web API - endpoints, middleware, error handling |
| `SchoolAccount.Collect.Application` | CQRS handlers and feature logic, organised by feature folder |
| `SchoolAccount.Collect.Domain` | Domain entities and business rules |
| `SchoolAccount.Collect.Infrastructure` | External concerns - time, data access, integrations |
| `SchoolAccount.Collect.SharedKernel` | Shared primitives - `Result<T>`, `Error`, `ValidationError` |

Each endpoint implements `IEndpoint` and is discovered and mapped automatically at startup. See
[Structure the solution using clean architecture](decisions/0002-use-clean-architecture.md) for the dependency rules.
Expand All @@ -152,5 +154,5 @@ is available at http://localhost:8081.
## Contributing

1. Branch from `main` using the convention `task/<short-description>` or `feature/<short-description>`.
2. Open a [pull request](https://github.com/DFE-Digital/SchoolAccount-ApiTemplate/pulls) against `main`.
2. Open a [pull request](https://github.com/DFE-Digital/SchoolAccount-CollectAPI/pulls) against `main`.
3. The [build workflow](.github/workflows/build.yml) must pass before merging.
16 changes: 8 additions & 8 deletions SchoolAccount.ApiTemplate.slnx → SchoolAccount.CollectAPI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/SchoolAccount.Application/SchoolAccount.Application.csproj" />
<Project Path="src/SchoolAccount.Domain/SchoolAccount.Domain.csproj" />
<Project Path="src/SchoolAccount.Infrastructure/SchoolAccount.Infrastructure.csproj" />
<Project Path="src/SchoolAccount.SharedKernel/SchoolAccount.SharedKernel.csproj" />
<Project Path="src/SchoolAccount.Web.Api/SchoolAccount.Web.Api.csproj" />
<Project Path="src\SchoolAccount.Collect.Api\SchoolAccount.Collect.Api.csproj" />
<Project Path="src\SchoolAccount.Collect.Application\SchoolAccount.Collect.Application.csproj" />
<Project Path="src\SchoolAccount.Collect.Domain\SchoolAccount.Collect.Domain.csproj" />
<Project Path="src\SchoolAccount.Collect.Infrastructure\SchoolAccount.Collect.Infrastructure.csproj" />
<Project Path="src\SchoolAccount.Collect.SharedKernel\SchoolAccount.Collect.SharedKernel.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SchoolAccount.Application.UnitTests/SchoolAccount.Application.UnitTests.csproj" />
<Project Path="tests/SchoolAccount.ArchitectureTests/SchoolAccount.ArchitectureTests.csproj" />
<Project Path="tests/SchoolAccount.Web.Api.IntegrationTests/SchoolAccount.Web.Api.IntegrationTests.csproj" />
<Project Path="tests\SchoolAccount.Collect.Api.IntegrationTests\SchoolAccount.Collect.Api.IntegrationTests.csproj" />
<Project Path="tests\SchoolAccount.Collect.Application.UnitTests\SchoolAccount.Collect.Application.UnitTests.csproj" />
<Project Path="tests\SchoolAccount.Collect.ArchitectureTests\SchoolAccount.Collect.ArchitectureTests.csproj" />
</Folder>
</Solution>
24 changes: 12 additions & 12 deletions decisions/0002-use-clean-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ can be enforced mechanically at the assembly level.
The solution is split into five projects, with all dependencies pointing
inward:

* `SchoolAccount.SharedKernel` - base building blocks (`Result`, `Error`) with no dependencies.
* `SchoolAccount.Domain` - entities and business rules; depends only on `SchoolAccount.SharedKernel`.
* `SchoolAccount.Application` - command/query handlers for each use case, implementing
* `SchoolAccount.Collect.SharedKernel` - base building blocks (`Result`, `Error`) with no dependencies.
* `SchoolAccount.Collect.Domain` - entities and business rules; depends only on `SchoolAccount.Collect.SharedKernel`.
* `SchoolAccount.Collect.Application` - command/query handlers for each use case, implementing
`ICommandHandler`/`IQueryHandler` interfaces. Handlers are decorated with cross-cutting
concerns such as validation and logging. Depends on `SchoolAccount.Domain` and
`SchoolAccount.SharedKernel`.
* `SchoolAccount.Infrastructure` - EF Core/PostgreSQL persistence, authentication, time -
concerns such as validation and logging. Depends on `SchoolAccount.Collect.Domain` and
`SchoolAccount.Collect.SharedKernel`.
* `SchoolAccount.Collect.Infrastructure` - EF Core/PostgreSQL persistence, authentication, time -
implementations of abstractions the inner layers define; depends on
`SchoolAccount.Application`.
* `SchoolAccount.Web.Api` - minimal API endpoints and composition root; references
`SchoolAccount.Infrastructure` only to wire dependency injection.
`SchoolAccount.Collect.Application`.
* `SchoolAccount.Collect.Api` - minimal API endpoints and composition root; references
`SchoolAccount.Collect.Infrastructure` only to wire dependency injection.

### Consequences

Expand All @@ -66,9 +66,9 @@ inward:

### Confirmation

`tests/SchoolAccount.ArchitectureTests` encodes the dependency rules with NetArchTest (e.g.
`SchoolAccount.Domain` must not depend on `SchoolAccount.Application`,
`SchoolAccount.Application` must not depend on `SchoolAccount.Infrastructure`). These tests run in the
`tests/SchoolAccount.Collect.ArchitectureTests` encodes the dependency rules with NetArchTest (e.g.
`SchoolAccount.Collect.Domain` must not depend on `SchoolAccount.Collect.Application`,
`SchoolAccount.Collect.Application` must not depend on `SchoolAccount.Collect.Infrastructure`). These tests run in the
`Build` workflow on every pull request, so a violating change cannot merge cleanly.

## More Information
Expand Down
8 changes: 4 additions & 4 deletions decisions/0007-validate-requests-with-data-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ Core validates them automatically before the endpoint delegate runs, with no dec
validator class needed for what is usually a handful of attributes.

`services.AddValidation()` in
[SchoolAccount.Web.Api/DependencyInjection.cs](../src/SchoolAccount.Web.Api/DependencyInjection.cs) wires
[SchoolAccount.Collect.Api/DependencyInjection.cs](../src/SchoolAccount.Collect.Api/DependencyInjection.cs) wires
up ASP.NET Core's built-in minimal API validation. `GetByLaestabRequest.Laestab` in
[GetByLaestabRequest.cs](../src/SchoolAccount.Web.Api/Endpoints/Organisations/GetByLaestab/GetByLaestabRequest.cs)
[GetByLaestabRequest.cs](../src/SchoolAccount.Collect.Api/Endpoints/Organisations/GetByLaestab/GetByLaestabRequest.cs)
carries a `[RegularExpression]` attribute as the example. A malformed value never reaches
`GetOrganisationByLaestabQueryHandler`; the endpoint's `.ProducesValidationProblem()` documents the
response the client gets instead.
Expand Down Expand Up @@ -87,14 +87,14 @@ up front.
anything needing other data (a database lookup, a cross-field business rule) has no attribute to
express it and must be checked inside the handler instead.
* Neutral, because `FluentValidation.DependencyInjectionExtensions` remains referenced in
[SchoolAccount.Application.csproj](../src/SchoolAccount.Application/SchoolAccount.Application.csproj)
[SchoolAccount.Collect.Application.csproj](../src/SchoolAccount.Collect.Application/SchoolAccount.Collect.Application.csproj)
for that case. Nothing currently
uses it; if a query or command needs it, wrap that specific handler the way `LoggingDecorator`
does, rather than making FluentValidation the default for every handler.

### Confirmation

[GetByLaestabTests.cs](../tests/SchoolAccount.Web.Api.IntegrationTests/EndPoints/Organisations/GetByLaestabTests.cs)
[GetByLaestabTests.cs](../tests/SchoolAccount.Collect.Api.IntegrationTests/EndPoints/Organisations/GetByLaestabTests.cs)
exercises the endpoint with a malformed LAESTAB value and asserts the validation problem response,
confirming the request never reaches the query handler.

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
services:
web-api:
collect-api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_HTTP_PORTS=8080
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
services:
web-api:
image: ${DOCKER_REGISTRY-}webapi
container_name: web-api
collect-api:
image: ${DOCKER_REGISTRY-}collectapi
container_name: collect-api
build:
context: .
dockerfile: src/SchoolAccount.Web.Api/Dockerfile
dockerfile: src/SchoolAccount.Collect.Api/Dockerfile
ports:
- "5100:8080"
- "5101:8080"

seq:
image: datalust/seq:2024.3
Expand Down
18 changes: 9 additions & 9 deletions docs/clean-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Infrastructure depends on both Domain and Application but is never referenced by

### 4. Presentation

The entry point to the system - the Web API project. In this solution that is `SchoolAccount.Web.Api`.
The entry point to the system - the Web API project. In this solution that is `SchoolAccount.Collect.Api`.

Contains:
- **Endpoints** - thin wrappers implementing `IEndpoint` that dispatch Commands and Queries
Expand All @@ -105,25 +105,25 @@ Domain ← Application ← Infrastructure

The arrows show the direction of **allowed** dependencies. No arrow ever points toward Infrastructure from the inner layers.

These rules are enforced by the tests in `tests/SchoolAccount.ArchitectureTests`.
These rules are enforced by the tests in `tests/SchoolAccount.Collect.ArchitectureTests`.

---

## How This Solution Applies Clean Architecture

| Layer | Project |
|----------------|--------------------------------|
| Domain | `SchoolAccount.Domain` |
| Application | `SchoolAccount.Application` |
| Infrastructure | `SchoolAccount.Infrastructure` |
| Presentation | `SchoolAccount.Web.Api` |
| Shared kernel | `SchoolAccount.SharedKernel` |
| Domain | `SchoolAccount.Collect.Domain` |
| Application | `SchoolAccount.Collect.Application` |
| Infrastructure | `SchoolAccount.Collect.Infrastructure` |
| Presentation | `SchoolAccount.Collect.Api` |
| Shared kernel | `SchoolAccount.Collect.SharedKernel` |

`SchoolAccount.SharedKernel` contains primitives used across all layers (`Result<T>`, `Error`, `ValidationError`,
`SchoolAccount.Collect.SharedKernel` contains primitives used across all layers (`Result<T>`, `Error`, `ValidationError`,
`IDateTimeProvider`) and sits outside the ring model - it has no dependencies itself and can be referenced by any
layer.

The Application layer organises features using **package by feature** under `src/SchoolAccount.Application/`,
The Application layer organises features using **package by feature** under `src/SchoolAccount.Collect.Application/`,
with each use case in its own folder.

---
Expand Down
4 changes: 2 additions & 2 deletions docs/integration-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Narrow integration testing ensures the stack is wired up correctly prior to runn

While targeted at MVC sites, the [Integration Testing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0&pivots=xunit) guide provides a good overview of the approach used in this project.

Rather than starting a web server and sending HTTP requests from a separate process the tests leverage the [WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0&pivots=xunit#basic-tests-with-the-default-webapplicationfactory) to host the API in-process by extending the API's [Program.cs](../src/SchoolAccount.Web.Api/Program.cs).
Rather than starting a web server and sending HTTP requests from a separate process the tests leverage the [WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0&pivots=xunit#basic-tests-with-the-default-webapplicationfactory) to host the API in-process by extending the API's [Program.cs](../src/SchoolAccount.Collect.Api/Program.cs).

This allows the tests to send HTTP requests directly to the API without the overhead of starting a separate process allowing tests run faster in an isolated fashion.

Expand All @@ -30,7 +30,7 @@ var client = _factory.WithWebHostBuilder(builder =>

For more advanced scenarios a [Custom WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0&pivots=xunit#customize-webapplicationfactory) may be created to provide common defaults across tests and reduce boilerplate setup.

The [SchoolAccount.Web.Api.IntegrationTests](../tests/SchoolAccount.Web.Api.IntegrationTests) project contains 'narrow' integration tests for the API endpoints.
The [SchoolAccount.Collect.Api.IntegrationTests](../tests/SchoolAccount.Collect.Api.IntegrationTests) project contains 'narrow' integration tests for the API endpoints.

Due to the simplicity of the example API the default WebApplicationFactory is sufficient, however the project demonstrates use of `ConfigureTestServices` to replace the handler with a test implementation.

Expand Down
Loading