Project to track people, whether that's students, team members or yourself.
- Student Progress
The project can be run with either dotnet itself or docker. It is recommended to run dotnet for development usecases and docker when runnen in production.
Before you're able to run the application, you have to apply migrations. See the Development section for more instructions.
To run the project, execute the following command in the StudentProgress.Web
folder:
dotnet run
or open the solution in an IDE and start from there.
To run the project, execute the following command in this folder:
docker-compose up -d
The app will be available on http://localhost:80
. The app and database will keep running until you stop with docker-compose down
or using the GUI
To build new changes into the running docker image, execute the following command:
docker-compose up -d --build
To make the project run, you are required to initially put Auth0
config values in the appsettings.Development.json
.
This appsettings file can be copied from appsettings.Development.example.json
.
There is no database configuration changes required, as the project uses Sqlite3 with EntityFramework.
You can, however, change the path of the database file in the appsettings
.json
TODO
To apply migrations:
dotnet ef database update --startup-project "./StudentProgress.Web/StudentProgress.Web.csproj" --project="./StudentProgress.Core/StudentProgress.Core.csproj"
To add migrations:
dotnet ef migrations add InitialCreate --startup-project "./StudentProgress.Web/StudentProgress.Web.csproj" --project="./StudentProgress.Core/StudentProgress.Core.csproj"
To remove migrations:
dotnet ef migrations remove --startup-project "./StudentProgress.Web/StudentProgress.Web.csproj" --project="./StudentProgress.Core/StudentProgress.Core.csproj"
Integration tests are done through a real database. Please be advised with using integration tests:
- Only create 2 tests for a usecase:
- One for the longest happy path
- One failure test. Preferrably the longest, but any will do.
- One exception to this rule is when there are a lot of database specific constraints, like db uniqueness
Creating a new integration test file is easy:
- Add a new class
- Add the attribute
[Collection("db")]
above the class definition - Implement abstract class
DatabaseTests
- You will have to add the constructor:
public <ClassName>(DatabaseFixture fixture) : base(fixture) {}
- You now have access to the
Fixture
property
Test are written using xUnit
Assertions are done with Fluent assertions
These combined, a test looks like this:
[Fact]
public void Name_cannot_be_empty() {
var name = Name.Create("");
name.IsSuccess.Should().BeFalse();
}
I've used a combination of C4 and abstract class diagram to highlight the most important parts of the technical design. Find more information about the c4 model here.
Note that the authentication can be disabled alltogether in the appsettings.Development.json
or the environment variables.
Documentation on the different technologies used can be found below:
- .NET 5 Razor Pages
- Authentication with OpenIDConnect
- Entity Framework Core 5.x
- Keycloak documentation
- PostgreSQL documentation
- Postgresql Npgsql Entity Framework Core Provider (the nuget package that makes Postgres work with EF)
To keep the diagram simple, only the creation or updating of a Progress entity has been modeled. However, you can apply the same principle to all entities (see Domain model for more info)
For each component some additional explanation is warranted:
This component is nothing more than a PageModel that you're used to from Razor Pages. Each PageModel communicates with the business logic in either one of the following two ways:
- A
ProgressContext
class for retrieving data and aUseCase
class for persisting data - A
UseCase
class for retrieving data and aUseCase
class for persisting data
The reason not all data retrieval has a seperate UseCase class is either I've been too lazy to create one or it was a very simple query that could be solved with Entity Framework. You should always aim to put everything in UseCase classes
A PageModel shouldn't be doing anything else except input validation and passing info to the UseCase classes.
UseCase classes are the meat of the system. They (should) speak to entities and persist data to the database.
Note that this application does not have a lot of business logic. However, I've tried to put all business logic in the entity classes and value objects.
Every UseCase class is always accompanied by the following three components:
Request
orCommand
class (depending on whether it's a GET or a POST)- A
Result
class - A
HandleAsync
method that accepts either theRequest
orCommand
class in the same file
This architectural principle has been heavily inspired from Jimmy Bogard's Vertical Slice Architecture. There are, however, some notable differences:
- The package
MediatR
hasn't been used. The reason for this is simply because I didn't see the need for this. I wanted tight coupling between PageModels and UseCases to make the application as simple as possible - I also haven't made use of any IoC/DI apart from the
ProgressContext
. The reason for this is simplicity as well. Integration tests are the most important part of a CRUD application and thus you don't need much - if any at all - interfaces or mockery for the application. This choice is heavily inspired by Vladimir Khorikov's book Unit Testing: Principles, Practices and Patterns
Finally, you might notice I've made a lot of use of the CSharpFunctionalExtensions Nuget package. This is also heavily inspired by Vladimir Khorikov, but also my love for functional programming.
See below the domain model of all the entities. Note that the ValueObjects and enums have been omitted to keep the model as simple as possible.
Below contains a Mermaid ER diagram of the functional relationships. The reason I call it functional is, because I intentionally left out the relationsihops that might clutter the diagram. For example, ProgressUpdate
also has a relationship with Group
. Another example is that MilestoneProgress
has relationships to Milestone
and Group
. Though again, this will only confuse the reader.
A MilestoneProgress cannot live without a ProgressUpdate
---
title: ER diagram
---
erDiagram
Group }o--o{ Student : has
Group ||--o{ Milestone : contains
Student ||--o{ ProgressUpdate : has
ProgressUpdate ||--o{ MilestoneProgress: has