Server components architecture¶
The source code is organized into several projects with clearly defined purpose in order to keep the architecture clean and easily changeable.
Solution overview¶
The server repository holds a single solution which contains source code for both server and worker components, and the communication layer between those two components.
Server component¶
The overall architecture of the server component follows the clean architecture practice. Projects are grouped by purpose into several groups with clearly defined allowed dependencies. The groups are listed below. Note that some of the projects are shared with the worker component.
| Domain: | Defines types and classes which are used throughout the server component.
|
|---|---|
| Application: | Contains all application specific logic.
|
| Infrastructure: | Implements communication services outisde of the application scope (sending emails).
|
| Persistence: | Implements persistence of the domain objects into a database.
|
| Presentation: | Exposes the application to it’s consumers.
|
Figure Structure and dependencies of projects in the server component. illustrates the relationship between the parts of the architecture, with arrows depicting allowed dependencies.
Direction of the arrows is very important aspect of the clean architecture. For example:
OPCAIC.Application project does not depend on the OPCAIC.Persistence project, even though
the application logic needs to communicate with database. This is achieved using Dependency
Inversion principle. The OPCAIC.Application project defines required functionality using C#
interfaces, and OPCAIC.Persistence provides implementation of these interfaces.
Worker component¶
Instances of the worker component create a pool of machines which are used for evaluating (potentially long running) tournament matches. The backend implementation is again separated to multiple projects.
| Worker: | Implementation of the Application logic to be executed on individual machines
|
|---|---|
| Broker: | Deployed as part of the server, manages worker pool, load balancing and dispatching of tasks to individual workers.
|
| Communication Layer: | |
Implements communication between Worker and Broker projects.
|
|
Figure Dependency diagram of worker component’s projects puts the projects listed above into context, including shared dependencies with the main server component.
Following sections will describe these projects in more details.
Server component¶
As outlined in the previous section, the architecture of the server is divided into projects based on the code’s responsibility. This section gives more detailed information about the inner designs of the code structures used in individual projects
OPCAIC.Domain¶
This project contains enums and classes which describe the individual entities in the domain. By
design, this project does not contain any application specific logic, or any logic concerning
persistence or serialization of the entities. Also, this project contains Enumeration and
ValueObject classes to be used throughout the project.
Enumerations¶
In some cases, simple lanugage provided enums are not appropriate, for example if enums are
used for a fixed list in a dropdown menu, simple changes like adding a new item require recompiling
and redeploying. In case the project does periodical releases with multiple staging environments,
such “simple change” may take weeks to get to production.
Inspired by many blog posts on why using enums is not alwasy optimal (e.g. Enums are Evil by
Thomas Weingartner), we implemented the
concept os smart enum, which not only is extensible during runtime (e.g. by loading members from
file or database). Simple example can be found in the GameType, which provides additional
information about the game type, like what tournament format does the type support. Another example
is the EmailType members of which can be also used as a factory for email data structures.
Value objects¶
Concept of value objects is taken from Domain Drive Design. A value object represents an entity
whose equality is based on the equality of it’s individual components. E.g. Two addresses are equal
if all its components (street name, city, ZIP code…). To make the implementation of such objects
easier, a ValueObject base class was adapted from the Microsoft’s blog post Implementing Value
Objects. This
base class requires implementing only the GetAtomicValues() method to get its individual
components. This also automatically works with inheritance hierarchies. For example, see
MenuItem class.
OPCAIC.Common¶
This small project contains cross-cutting concerns and definitions of EventIds and tag names
for structured logging purposes.
OPCAIC.Persistence¶
This project encapsulates communication with database and how entities defined in OPCAIC.Domain
are mapped to database tables. This project uses Entity Framework Core, which is an Object-Relational Mapper (ORM) library. This
library encapsulates differences between individual (not only) SQL databases.
OPCAIC.Infrastructure¶
This project implements communication with services outside of the application. Currently, there is only logic concerning sending email notifications.
OPCAIC.Application¶
This object contains main application logic. Instead of more traditional N-tier architecture, this
project uses so called Vertical slices architecture. Instead of encapsulating logic into layers
like service layer and database layer, vertical slices encapuslates code based on it’s business
use-case. For example, there is a CreateTournamentCommand class which represents a request to
create a new tournament. then there is a corresponding CreateTournamentCommand.Handler class
wich is able to carry out this request and encapsulates all needed logic.
In Vertical Slices Architecture, adding a new feature should not require modifying existing code, since the new logic should be encapsulated in new request and handler classes.
The individual handlers for each use case are organized based on the entity on which operates, and then categorized based on whether it is a command (request which modifies data) or query.
MediatR¶
The application project makes heavy use of the MediatR library, which implements automatic discovery of request handlers and dispatch of request objects to them. This makes the coupling very loose between the handlers and the code that needs the logic they provide.
AutoMapper¶
Another library which is heavily used in the project is AutoMapper. This library simplifies mapping between two objects. When using automapper, there is no need to manually list all properties which need to be copied from one instance to the other and risk forgetting to update some code when a new property is added to a type.
Mappings between types have to be specified beforehand in order to be compiled. This is both for
performance reasons and protecting against unintended mappings. However, having a central file where
these mappings are listed is not optimal, because the mapping is not visible when looking at class
definition. Also the centralized solution fails when one of the classes is not visible due to the
directions of dependencies between projects. To resolve the situation, two empty interfaces
IMapTo<TDestination> and IMapFrom<TSource> can be implemented to mark that a map should be
created between given types. If the mapping requires special configuration, interface
ICustomMapping can be used to specify any mapping in its CreateMapping method. These
interfaces are then examined on startup and appropriate mappings created in the
AutoMapperProfile class.
Database query specifications¶
As we noted earlier, the OPCAIC.Persistence is refernces (is dependent) on the OPCAIC.Application
project. The application project therefore defines interface for accessing the database and
persistence project provides the implementation.
However, The straightforward implementation of this idea could lead to many single-purpose methods on the interface, like GetTournamentsForUseCaseX, GetTournamentsForUseCaseY, and adding new functionality would ultimately require extending the said interface.
The above problem was solved using the specification pattern. The application defines
ISpecification<TEntity> interfaces used to describe the query. The description includes:
- filtering criteria on the database table
- ordering specification (to get results sorted),
- and offset and number of items to fetch (to support paginated requests)
The communication with database happens via IRepository<TEntity> interface which accepts the
specification objects. The underlying IRepositoryTEntity> implementation then uses the
information from specification object to query the database and return the results back to the
caller.
There is alsoIProjectingSpecification<TEntity, TDestination>, which is a variant of theISpecification<TEntity>interface which adds projecting specification and allows for some transformation of the queried data, like fetching only subset of database columns.
This way, the details of database queries like filtering criteria still reside inside the application project and there is no need to modify interfaces between application and persistence projects when the query changes.
Unit tests¶
The solution contains comprehensive set of unit tests. Each project in the solution has a dedicated
test project under the test folder, with the exception of OCPAIC.Common, which does not
contain any testable logic. The unit tests make heavy use of the Moq library for mocking dependencies of the class under
test.
In addition to regular unit tests which concentrate on functionality of a single class, there is the
OPCAIC.FunctionalTest project. Tests in this project are targeted on the server as a
whole. These tests start the server and test it’s functionalit by sending HTTP requests analyzing
the server’s responses.