SchoolAccount-Web is an MVC presentation application for the DfE School Account service built on .NET 10. It uses a minimal clean architecture solution, with CQRS abstractions, structured logging, error handling, and architecture tests.
- Clean Architecture - layers, dependency rules, and code organisation
- Coding Standards - formatting, code analysis, naming, and style conventions
- Contributing - guidance on repository branching strategies
- Testing Standards - conventions and practices for writing tests
- Integration Testing - guidance on integration testing of the controller endpoints
- Feature Flags - adding, gating, testing, switching and removing feature flags
Architecture decisions are recorded as ADRs in the decisions folder:
- Use Markdown Architectural Decision Records - why and how we record decisions
- Structure the solution using clean architecture - layers, dependency rules, and code organisation
- Strip the imported template to a minimal core - what was removed from the original template and why
- Run tests on the Microsoft Testing Platform - testing platform and how results and coverage are reported in CI
- Format code with CSharpier - why formatting is automated and enforced in the build
- Enforce code quality with Roslyn analysers - why SonarAnalyzer.CSharp and strict analysis are enforced in the build
- Supporting SASS within GDS Styles - why SASS support has been enabled
- Use feature structure - why the feature structure has been adopted
- Authentication using DSI - why we authenticate with DfE Sign-In
- Use containerisation to publish code - why we have chosen to use containerisation and push to the github container registry
- Integrate with the Academies API - why we have used the Academies API
- Use the Azure Monitor OpenTelemetry distro for telemetry - how logs, traces and metrics reach Azure Monitor and Rider
- Use feature flags from Azure App Configuration - how features are switched on and off per environment without a release
New decisions should follow the ADR template.
Follow these steps to start the MVC locally.
Note: Windows users can use the git bash command prompt to run the project's .sh bash scripts.
-
Install prerequisites:
- .NET 10 SDK
- Docker Desktop
- Rider, Visual Studio, or Visual Studio Code
- Pull Collect API repository
-
Run the setup script from the repository root to restore the dotnet tools and enable the git hooks:
./init.sh
-
Run the MVC using one of the following:
Method Command Outcome Docker Compose docker compose up --buildStarts the MVC and its dependencies (Seq) in containers .NET CLI dotnet run --project src/SchoolAccount.Web.MvcRuns the MVC directly using the httplaunch profile, no containersIn Rider or Visual Studio you can use the equivalent
docker-composeorhttprun configurations from the toolbar. -
Launch Collect API.
-
Once running, the presentation is available at
http://localhost:5016:- Logs (if started with compose) at
http://localhost:8081
- Logs (if started with compose) at
-
Debugging guidance:
- Set breakpoints in your C# files under
src/and start either run configuration with debugging enabled.
- Set breakpoints in your C# files under
To ensure the feedback component is functional locally, there are tools that can be used to see this journey.
-
Install prerequisites:
-
Go to the root of the LocalDevTools repository and run this command to start Azurite and seed an initial table:
docker compose up
-
Open Azure Storage Explorer and connect to the Azurite instance running on
http://127.0.0.1:10002/devstoreaccount1/ -
To set your own
TableStorageConnectionStringuse the .NET user secrets -
Use the feedback component and check Azure Storage Explorer to see your new data
The project requires the following app configuration values to be overridden, ideally using User Secrets:
{
"OpenIDConnectSettings": {
"Authority": "<URL-OF-DSI-OIDC-SERVER>",
"ClientId": "<DSI-CLIENT-ID>"
},
"AcademiesApiClient": {
"BaseUrl": "<URL-OF-ACADEMIES-API>",
"ApiKey": "<ACADEMIES-API-KEY>"
}
}Use the .NET CLI to build or test the solution.
-
To build locally:
dotnet build
-
To run all tests:
dotnet test
Architecture tests under tests/SchoolAccount.ArchitectureTests enforce the clean architecture dependency rules between layers.
Code is formatted with CSharpier, installed as a local dotnet tool and enforced by the "Check formatting" step in the build workflow. To format the solution locally:
dotnet csharpier format .A pre-commit hook, managed by Husky.NET and configured in .husky/task-runner.json, formats staged C# files automatically before each commit; init.sh installs it and restores the tools on a fresh clone. To format on save, install the Rider plugin or the VS Code extension; the editors documentation covers setup for these and other IDEs. See Format code with CSharpier for the reasoning.
The build runs with the full set of .NET/Roslyn analyzers (AnalysisLevel=latest, AnalysisMode=All) plus
SonarAnalyzer.CSharp, configured in
Directory.Build.props. TreatWarningsAsErrors means any violation fails dotnet build,
locally and in the build workflow, rather than being left as a warning. Rules that
don't fit this codebase are suppressed by ID in .editorconfig. See
Coding Standards and
Enforce code quality with Roslyn analysers for the
conventions and the reasoning.
The build workflow collects code coverage on every run, posts a summary to the pull
request, and fails the build if line coverage drops below the minimum threshold. The threshold is defined by the
MIN_LINE_COVERAGE variable at the top of build.yml. Which files are included is
controlled by coverage.config.
To generate the same report locally, run coverage.sh from the repository root:
./coverage.shTo ensure that the same report is generated locally, ensure you have the latest version of dotnet installed.
The script runs all tests with coverage enabled, merges the per-project results with ReportGenerator, and writes an
HTML report to TestResults/CoverageReport/index.html. Pass --open to open the report in your browser when it
finishes:
./coverage.sh --openThe solution follows a clean architecture pattern with vertical slice features:
| Project | Purpose |
|---|---|
SchoolAccount.Web.Mvc |
ASP.NET Core Web MVC - controllers, 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 |
Structured logs are written via Serilog to Seq. When running via Docker Compose, the Seq UI is available at http://localhost:8081.
The Web project is published to the GitHub Container Registry
as a Docker image that can be pulled down and run.
The image is created on every push to the main branch and tagged with the current commit SHA and the latest tag.
To verify the image locally, you can run:
docker run --platform linux/amd64 --name web -e OpenIDConnectSettings__ClientId=SA_TEST_CLIENT -e OpenIDConnectSettings__Authority=https://localhost:7041 -e ASPNETCORE_ENVIRONMENT=development -p 5100:8080 -d --pull=always ghcr.io/dfe-digital/schoolaccount-web:latest
You can then test the Web project by visiting http://localhost:5100.
To stop and delete the container, you can run:
docker stop web
docker rm web
Dependabot checks NuGet packages and GitHub Actions weekly, and groups updates to keep the number of pull requests down. A NuGet package goes in the first group it matches:
| Group | What's in it | Update types |
|---|---|---|
test-tooling |
Test libraries | All |
dotnet |
Packages released with each version of .NET, such as Microsoft.AspNetCore.* |
All |
azure |
Azure SDK packages, which all build on Azure.Core |
All |
serilog |
Serilog and its sinks | All |
fluentvalidation |
FluentValidation and its DI extensions | All |
opentelemetry |
OpenTelemetry exporters and instrumentation | All |
minor-and-patch |
Every other package, except SonarAnalyzer.CSharp |
Minor, patch |
| (no group) | Major updates to other packages, and every SonarAnalyzer.CSharp update |
One PR each |
SonarAnalyzer.CSharp gets its own pull requests because a new release can add rules that fail the build, which would
hold up every other update in a group. GitHub Actions minor and patch updates arrive together in one pull request.
Directory.Packages.props has one labelled ItemGroup per group, so the grouping is visible
where versions are set. When you add a package:
- Add its version to the
ItemGroupfor its group, or tootherif none fits. - If it went into a named group, check that a pattern in that group in dependabot.yml
matches its name, and add one if not. Packages in
otherneed no change, becauseminor-and-patchcovers them.
Use the exact package name for a single package (for example "Shouldly"), or end it with * to match a family of
packages (for example "Microsoft.Testing.*").
- Branch from
mainusing the conventiontask/<short-description>orfeature/<short-description>. - Open a pull request against
main. - The build workflow must pass before merging.
Some components within the solution are third-party libraries, which cannot be managed via NuGet packages. These
will be self-managed; listed below and are located within the solution under a lib folder.
| Name | Version / Date Added | Source |
|---|---|---|
| Step By Step Navigation | Thu 3rd Sep 2026 | https://github.com/alphagov/govuk-prototype-kit-step-by-step |
Support for npm packages may be added in the future.