Welcome to the tenth episode of my course “Becoming a software developer” in which we will start designing our domain and services.
All of the materials including videos and sample projects can be downloaded from here.
The source code repository is being hosted on GitHub.
Scope
- Domain
- Services
- Controllers
Abstract
Domain
As I mentioned already before, we will incorporate the Domain Driven Design (DDD) approach (at least in its light version) into our solution. Our domain models are the root of the Core project. They will have to be rich classes (that we will be continuously refactoring throughout the whole course), containing both properties and methods that will ensure that the internal of such object is valid. Otherwise, our domain model might throw an exception in order to let the user know that something went wrong. We can split our domain models into the following types:
- Value Object – has no unique identifier, is immutable, can represent address, geolocation etc. Both Value Objects are usually equal if all of their properties have the same values.
- Entity – has the unique identifier, which means that both entities are equal only if they possess the same id. This is a rich model that may contain additional methods in order to manipulate its state.
- Aggregate – it’s an entity that may contain other entities and is a root model that we will have access to via the repository. It can be constructed by using one or more entities. Think about the Trip which can be an entity and have it’s unique identifier, but without a Driver (aggregate) the Trip on it’s own doesn’t make much sense – this is what the aggregates are for, to set the boundaries and ensure the valid state of the other entities that are a part of it.
Our repository will also contain the repositories, but only the interfaces. The actual implementation of such repository interface is the concern of the Infrastructure layer, as it could be either in memory, database or even some external storage being used to save our data.
Later on, we may also include e.g. events in order to build a more sophisticated application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class User { public Guid Id { get; protected set; } public string Email { get; protected set; } public string Password { get; protected set; } public string Salt { get; protected set; } public string Username { get; protected set; } public string FullName { get; protected set; } public DateTime CreatedAt { get; protected set; } protected User() { } public User(string email, string username, string password, string salt) { Id = Guid.NewGuid(); Email = email.ToLowerInvariant(); Username = username; Password = password; Salt = salt; CreatedAt = DateTime.UtcNow; } } public interface IUserRepository { User Get(Guid id); User Get(string email); IEnumerable<User> GetAll(); void Add(User user); void Update(User user); void Remove(Guid id); } |
Services
Our infrastructure will have to deal with a lot of different tasks related either to implementation of the domain interfaces, handling the database connections and so on.
One of such requirements will be to provide the so-called application services. They will be defined as interfaces and implemented using the domain repositories and models in order to manipulate them and perform the actual business logic (e.g. registering user, creating a new trip etc.). The important part here is that we will never expose the domain itself to the higher layer which would be API in our solution (or the UI). We want to ensure the consumer of the application services that the models he will receive will be safe to use, which means that they will be just a set of public properties with no robust methods whatsoever. By doing so, we can be certain that we will never modify our domain model (even unwillingly) via our API without ensuring it’s proper validation and the overall flow. In order to achieve such goal, we will be returning DTO (Data Transfer Objects) from our infrastructural services to the other layers (being API in that scenario).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class UserDto { public Guid Id { get; set; } public string Email { get; set; } public string Username { get; set; } public string FullName { get; set; } } public interface IUserService { UserDto Get(string email); void Register(string email, string username, string password); } public class UserService : IUserService { private readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; } public UserDto Get(string email) { var user = _userRepository.Get(email); return new UserDto { Id = user.Id, Username = user.Username, Email = user.Email, FullName = user.FullName }; } public void Register(string email, string username, string password) { var user = _userRepository.Get(email); if(user != null) { throw new Exception($"User with email: '{email}' already exists."); } var salt = Guid.NewGuid().ToString("N"); user = new User(email, username, password, salt); _userRepository.Add(user); |
Controllers
This is where we can actually deal with our application from the end user point of view. For now, we can only do the very trivial things such as fetch the User account, but later on we will be able to do much, much more. Our controllers define the set of distinct operations (GET, PUT, POST, DELETE) and each one of them will have its unique endpoint called the URI (Unique Resource Identifier) e.g. /users or /drivers/{id}/vehicle that will allow to either get or modify the underlying data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[Route("[controller]")] public class UsersController : Controller { private readonly IUserService _userService; public UsersController(IUserService userService) { _userService = userService; } [HttpGet("{email}")] public async Task<UserDto> Get(string email) => await _userService.GetAsync(email); } |
Next
In the next episode, we’ll make use of some external libraries to help us with handling the DTO mapping, take a look at the HTTP POST method and refactor our services to be fully asynchronous.
Pingback: Becoming a software developer – episode X – Patryk Huzarski | Personal Blog
Pingback: Dew Drop - March 30, 2017 (#2451) - Morning Dew
Hej!
Mam szybkie pytanie jeśli to możliwe? W trakcie kursu piszę swoją apkę tak by na bieżąco wdrażać wiedzę i natrafiłem na gwoździa. Mam issues powiązane z moim userem ale które pobieram z zewnętrznego serwisu i nie będę ich przechowywał w mojej bazie danych – gdzie w onnion architecture powinny być klasy odpowiedzialne za te issues?
Pozdrawiam.
Cześć,
Po prostu w warstwie infrastruktury stwórz np. nowy serwis aplikacji oczywiście opakowany odpowiednim interfejsem, który będzie pobierał w tym przypadku dane z zewnętrznego źródła, najprostsze rozwiązanie.
Pozdrawiam
Aha!! Rozszerzam wtedy po prostu Users w UsersDTO o listę Issues pobranych z zewnętrznego Api których definicja (klasa z propertisami) jest w infrastrukturze. Super bardzo to elastyczne. Dzięki za odpowiedz!!
Dokładnie tak, dla warstwy API/prezentacji zupełnie niestotne jest to skąd pochodzą dane, tym zarządza tylko Infrastruktura :).
Hej!
Mam pytanie według wytycznych od ms który zaleca clean architecture dla aplikacji web warstwa UI ma referencje do CORE i INFRA natomiast w architekturze zaproponowanej przez ciebie tylko do INFRA. Podobna różnica jest odnośnie miejsca przechowywania klas DTO według ms w CORE a w passenger w INFRA. Możesz pokrótce odnieść się do tych różnic?
Cześć,
Ja się stosuję do Onion Architecture. Warstwa taka jak API/UI nie powinna nic wiedzieć o Core (czyli głównie o domenie). Przykładowo API jedyne co musi wiedzieć, to w jaki sposób wykonać logikę biznesową oraz pobrać ewentualne dane. W tym celu wykorzystuje się DTO, które podobnie jak serwisy aplikacji wchodzą w skład tzw. anti-corruption layer.
Witam.
A skąd w Startup wziął się:
using Passenger.Core.Repositories;
W końcu Api nie powinno widzieć Core?
Do wywalenia, nie jest do niczego potrzebne.
Ok. Ale dlaczego w ogóle można w API dodać using do Core?
Spowodowane jest to tym że skoro API zależy od Infrastructure to pomimo iż nie jest zależne bezpośrednio to i tak ma dostęp do jego ‘wnętrza’?
Bo przeczy to założeniu że API nic nie wie o Core 🙁
A tak na marginesie to kurs genialny, bardzo mi pomógł. Dzięki wielkie.
Pozdrawiam
Hi! I have a question regarding the GUIDs in CQS. Having non sequential GUID in DB could be problematic in the future. So this is a good idea to use Id as an int. When we need to insert new object into the DB what is the best practice to send inserted ID back to our UI and still using CQS pattern? As you said command shouldn’t return any value.