Right now this repository is lagging behind and does not get deserved love because I have enaged in improving NetArchTest or even rewriting Typewriter/NTypewriter tools that I use here.
Yet another sample .net core application. But this one aims to be a little bit different than the rest. Instead of focusing on showing some fancy libraries and patterns on non-realistic simplified examples, this one focus on delivering a fully working application with high quality.
- How to run
- Overview
- Architecture decision record
- Layer : Presentation.React
- Layer : Presentation.API
- Module comparison
- Communication between modules
- Module : UserManagement
- Module : TestCreation
- Module : TestConducting
- Module : TestResults
- Working demo
- ToDo
- external dependencies (postgres, rabbitmq, elasticsearch, kibana)
docker-compose up
- backend
green arrow in VS
- frontend
cd TestMe.Presentation.React/ClientApp/ && npm start
component | run | endpoint |
---|---|---|
.net core | from VS | https://localhost:44357 |
swagger | from VS | https://localhost:44357/swagger |
postgres | docker-compose up | n/a |
rabbitmq | docker-compose up | http://localhost:15672/ |
elasticsearch | docker-compose up | n/a |
kibana | docker-compose up | http://localhost:5601 |
influxdb | n/a | n/a |
react | npm start | http://localhost:3000/ |
jest | npm test | n/a |
storybook | npm run storybook | http://localhost:9009 |
- top architecture: modular monolith aka vertical slice architecture
- backend: asp.net core, entity framework core, postgresql
- tests: mstest v2, sqllite in-memory, detroit school of testing
- frontend: react + typescript
- communication between modules : RabbitMQ or in-memory bus
- enabled non-null reference types
- every module in separate transaction scope, eventual consistency between modules
- mapping between c# dtos is done by MappingGenerator
- typed api client for api calls is auto-generated with NTypewriter
TBD
- with restrictive Content Security Policy set (no inline css or js, no eval)
- CSS Modules (scoping css per component)
- every page implemented in two ways:
- class-components with local state
- functionals-components with redux and hooks
- component state persisted in localstorage
- props dependency injection used to be able to display stateful components in Storybook
- files grouped by features/routes and not by file type
- TypeScript all the way
- integration tests for happy paths backed on sqllite in-memory with the possibility to switch to postgresql if needed for debuging
Endpoint | |
---|---|
/QuestionsCatalogs/ | |
/Questions/ | Endpoint that allows editing whole Question aggregate (Question + Answer) as a single resource, with optimistic concurrency control |
/TestsCatalogs/ | |
/Tests/ | Endpoint that allows editing only aggregate root from Test aggregate (Test + QuestionItem) |
/Tests/{testId}/questions/ | Endpoint that allows editing TestItem entity from Test aggregate (Test + QuestionItem) as a sub-resource |
/Tokens/ | |
/Metrics/lineprotocol/ | A special endpoint available only from localhost that returns following metrics : - CPU usage, - RAM usage, - no. GC collections, - GC heaps sizes, - GC pause time, - GC background time, - ThreadpoolThreadCount, - ThreadpoolQueueLength, - ExceptionCount, - MonitorLockContentionCount in a format that can be directly pulled by Telegraf to InfluxDB |
/Users/ |
- server : vps from OVH, 2 GB RAM, 1 CPU Haswell 2,4GHz, Ubuntu 18.04.3, Apache/2.4.29 as reverse proxy
A constant client count: | 400 | 500 | 600 | 700 | 800 |
---|---|---|---|---|---|
Sync - Response Times Average (ms) | 545 | 703 | 850 | 1021 | 1860 |
Sync - Req/s | 728 | 703 | 697 | 675 | 418 |
Async - Response Times Average (ms) | 651 | 802 | 947 | 1096 | 2144 |
Async - Req/s | 609 | 618 | 626 | 632 | 359 |
A constant client count: | 400 | 500 | 600 | 700 | 800 |
---|---|---|---|---|---|
Sync - Response Times Average (ms) | 857 | 1171 | 1375 | 1640 | 2590 |
Sync - Req/s | 455 | 423 | 428 | 421 | 300 |
Async - Response Times Average (ms) | 967 | 1204 | 1364 | 1633 | 2574 |
Async - Req/s | 408 | 410 | 433 | 422 | 301 |
A constant client count: | 400 | 500 | 600 | 700 | 800 |
---|---|---|---|---|---|
Sync - Response Times Average (ms) | 1167 | 1536 | 1802 | 2071 | 2962 |
Sync - Req/s | 339 | 321 | 331 | 328 | 261 |
Async - Response Times Average (ms) | |||||
Async - Req/s |
Description | UserManagement | TestCreation |
---|---|---|
architecture | layers + transaction script | clean architecture + minimal CQRS |
domain model | anemic + a few value objects | rich |
data access layer | Entity Framework | Repository + unit of work for writes / Entity Framework for reads |
exceptional situations | DomainException | Result + DomainException |
pagination | cursor based | offset based |
- reliable communication between modules without using a distributed transaction
- two available implementations, RabbitMQ based for production use and in-memory for tests
- domain model: anemic
- architecture : layers + transaction script
- data driven unit tests and architectural tests
- outbox pattern for publishing integration events in a reliable way without using two phase commit - at least one delivery
- domain model: rich
- architecture : clean architecture + minimal CQRS (with the same data store)
- data driven unit tests and architectural tests
- soft delete for all entities
- optimistic concurrency and conflic resloving for Question aggregate
- inbox pattern for receiving integration events in a reliable way without using two phase commit - deduplication of events
not available yet
not available yet
- setup development environment in docker (postgresql, TICK stack, RabbitMQ, Elastic stack)
- introduce Architecture decision record (ADR)
- frontend : automated tests for frontend (maybe Cypress?)
- frontend : use immerjs to create next immutable state instead of home made solution
- backend : finish /Tests endpoints
- backend : deal with poisonous integration events (dead letter queue)
- backend : add batch publishing of events on RabbitMQ
- backend : improve in-memory bus implementation