loading...
Cover image for E036 - Making things more object oriented with rich domain entities - ASPF02O

E036 - Making things more object oriented with rich domain entities - ASPF02O

joaofbantunes profile image João Antunes Originally published at blog.codingmilitia.com on ・7 min read

ASP.NET Core: From 0 to overkill (45 Part Series)

1) ASP.NET Core: From 0 to overkill - Intro 2) Episode 001 - The Reference Project - ASP.NET Core: From 0 to overkill 3 ... 43 3) Episode 002 - Project structure plus first application - ASP.NET Core: From 0 to overkill 4) Episode 003 - First steps with MVC - ASP.NET Core: From 0 to overkill 5) Episode 004 - The Program and Startup classes - ASP.NET Core: From 0 to overkill 6) Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill 7) Episode 006 - Configuration - ASP.NET Core: From 0 to overkill 8) Episode 007 - Logging - ASP.NET Core: From 0 to overkill 9) Episode 008 - Middlewares - ASP.NET Core: From 0 to overkill 10) Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill 11) Episode 010 - Async all the things - ASP.NET Core: From 0 to overkill 12) Episode 011 - Data access with Entity Framework Core - ASP.NET Core: From 0 to overkill 13) Episode 012 - Move to a Web API - ASP.NET Core: From 0 to overkill 14) Episode 013 - Starting the frontend with Vue.js - ASP.NET Core: From 0 to overkill 15) Episode 014 - Centralizing frontend state with Vuex - ASP.NET Core: From 0 to overkill 16) Episode 015 - Calling the Web API from the frontend - ASP.NET Core: From 0 to overkill 17) Episode 016 - Authentication with Identity and Razor Pages - ASP.NET Core: From 0 to overkill 18) Episode 017 - More Identity, more Razor Pages - ASP.NET Core: From 0 to overkill 19) Episode 018 - Internationalization - ASP.NET Core: From 0 to overkill 20) Episode 019 - Roles, claims and policies - ASP.NET Core: From 0 to overkill 21) Episode 020 - The backend for frontend and the HttpClient - ASP.NET Core: From 0 to overkill 22) Episode 021 - Integrating IdentityServer4 - Part 1 - Overview - ASP.NET Core: From 0 to overkill 23) Episode 022 - Integrating IdentityServer4 - Part 2 - Auth Service - ASP.NET Core: From 0 to overkill 24) Episode 023 - Integrating IdentityServer4 - Part 3 - API - ASP.NET Core: From 0 to overkill 25) Episode 024 - Integrating IdentityServer4 - Part 4 - Back for Front - ASP.NET Core: From 0 to overkill 26) Episode 025 - Integrating IdentityServer4 - Part 5 - Frontend - ASP.NET Core: From 0 to overkill 27) Episode 026 - Getting started with Docker - ASP.NET Core: From 0 to overkill 28) Episode 027 - Up and running with Docker Compose - ASP.NET Core: From 0 to overkill 29) Episode 028 - Multiple service instances tweaks - ASP.NET Core: From 0 to overkill 30) Episode 029 - Simplifying the BFF with ProxyKit - ASP.NET Core: From 0 to overkill 31) Episode 030 - Analyzing performance with BenchmarkDotNet - ASP.NET Core: From 0 to overkill 32) Episode 031 - Some simple unit tests with xUnit - ASP.NET Core: From 0 to overkill 33) Episode 032 - Upgrading to ASP.NET Core 3.0 - ASP.NET Core: From 0 to overkill 34) E033 - Redesigning the API: Improving the internal architecture - ASPF02O 35) E034 - Segregating use cases with MediatR - ASPF02O 36) E035 - Experimenting with (yet) another approach to data access organization - ASPF02O 37) E036 - Making things more object oriented with rich domain entities - ASPF02O 38) Better use of types - avoiding nulls with an Optional type - ASPF02O|E037 39) More explicit domain error handling and fewer exceptions with Either and Error types [ASPF02O|E038] 40) Event-driven integration - Overview [ASPF02O|E039] 41) Event-driven integration #1 - Intro to the transactional outbox pattern [ASPF02O|E040] 42) Event-driven integration #2 - Inferring events from EF Core changes [ASPF02O|E041] 43) Event-driven integration #3 - Storing events in the outbox table [ASPF02O|E042] 44) Event-driven integration #4 - Outbox publisher (feat. IHostedService & Channels) [ASPF02O|E043] 45) Event-driven integration #5 - Quick intro to Apache Kafka [ASPF02O|E044]

In this episode, we'll make things more object oriented, by moving some logic that's present in the request handlers, which in fact should be present in the domain entities, that currently are just bags of data with public getters and setters.

Note: depending on your preference, you can check out the following video or skip it to the written version below.

The playlist for the whole series is here.

Intro

One thing I've been noticing for a while now, is our tendency to put all logic inside services (or like we've seen recently, request handlers), leaving the classes that represent our domain entities as simple bags of properties that we pass around. I've written about it here, but now it's time to apply that to this series code.

Moving the logic to the domain entities is something we read about when looking at DDD (domain driven design), resulting in what's called a rich domain model, but I'd argue it's not specific to this context. DDD literature commonly uses object oriented programming languages, but it can be implemented in other paradigms (e.g. functional programming). When using OOP languages however, it makes sense to use this paradigm's best practices, and probably the most important ones are to use the objects to represent specific concepts, encapsulate data and expose behavior through a public API.

When we put all the logic inside services or similar concepts, the code ends up being more procedural than object oriented. There's nothing wrong with procedural, but for the typical line of business applications (and others), modeling concepts using object oriented practices works well.

Moving domain logic into entities

So far, in the group management API, all the domain logic is part of the use case handlers. The goal for this episode is to reduce the logic contained in the handlers to the entities, making the handlers focus on orchestrating the interactions with the entities, repositories and so on.

Entities

As the application is far from complex at this point, the focus is on the Group entity. I'll drop the code below and then go through it.

Group.cs

public class Group : IVersionedEntity
{
    private readonly List<GroupUser> _groupUsers = new List<GroupUser>();

    public Group(string name, User creator)
    {
        Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException(nameof(name));
        Creator = creator ?? throw new ArgumentNullException(nameof(creator));
        _groupUsers.Add(GroupUser.NewAdministrator(Id, creator.Id));
    }

    public long Id { get; private set; }
    public string Name { get; private set; }
    public uint RowVersion { get; private set; }
    public User Creator { get; private set; }
    public IReadOnlyCollection<GroupUser> GroupUsers => _groupUsers.AsReadOnly();

    public void Rename(User editingUser, string newName)
    {
        ThrowIfNotAdmin(editingUser.Id);

        Name = !string.IsNullOrWhiteSpace(newName) ? newName : throw new ArgumentNullException(nameof(newName));
    }

    public bool IsAdmin(string userId)
        => GroupUsers.Any(gu => gu.UserId == userId && gu.Role == GroupUserRole.Admin);

    // TODO: temporary we'll get rid of all these exceptions eventually
    private void ThrowIfNotAdmin(string userId)
    {
        if (!IsAdmin(userId))
        {
            throw new UnauthorizedAccessException("User is not authorized to edit this group");
        }
    }
}

Let's begin with the properties, as those were the only things that were there already. All the properties are still there, but all of them now have private setters (minus GroupUsers, which doesn't even have that). The goal is to make all the changes to the entity data go through explicitly exposed APIs, that take into consideration the domain rules, rather than everything being modifiable through setters. On that note, the GroupUsers property not only doesn't have a setter, but the type was changed to be an IReadOnlyCollection<GroupUser>, so the collection can't be modified from outside the entity.

We now have a constructor that receives the name of the group and the user that is creating it. Not only is this necessary as we've disabled access to the properties, but it allows us to abstract the caller from the need to set the creator and add it to the list of group users, the constructor also encapsulates that piece of domain logic.

Continuing onwards, we have a couple more public methods, Rename and IsAdmin. Rename not only has a name that clearly states its intent (for example SetName would be a much more generic name), it will enforce the rules regarding renaming the entity, like requiring the editor to be an administrator, as well as the new name not being empty. With a public setter we wouldn't be able to achieve all of these characteristics. As for the IsAdmin method, it simply centralizes the logic to check if a user is an administrator, avoiding its repetition in the handlers that require such information.

The rest of the entities are not as interesting as the group entity, but they follow the same logic: encapsulate data, expose a public API with the available capabilities.

One final note for the GroupUser class (code below), that exposes a couple of helper factory methods, NewAdministrator and NewParticipant, which again serve to make the capabilities explicit. We could easily call the constructor passing in the role, but using these kinds of methods can simplify the calling code and make it more readable. Naming is important (and hard 😛).

GroupUser.cs

public class GroupUser
{
    public GroupUser(long groupId, string userId, GroupUserRole role)
    {
        GroupId = groupId;
        UserId = !string.IsNullOrWhiteSpace(userId) ? userId : throw new ArgumentNullException(nameof(userId));
        Role = role;
    }

    public long GroupId { get; private set; }
    public string UserId { get; private set; }
    public GroupUserRole Role { get; private set; }

    public static GroupUser NewAdministrator(long groupId, string userId)
        => new GroupUser(groupId, userId, GroupUserRole.Admin);

    public static GroupUser NewParticipant(long groupId, string userId)
        => new GroupUser(groupId, userId, GroupUserRole.Participant);
}

Use case handlers

Now that we moved a bunch of logic to the entities where it belongs, we can adjust the handlers to make use of it. We'll use UpdateGroupDetailsCommandHandler as an example, as the others follow the same approach.

UpdateGroupDetailsCommandHandler.cs

public sealed class UpdateGroupDetailsCommandHandler
        : IRequestHandler<UpdateGroupDetailsCommand, UpdateGroupDetailsCommandResult>
{
    public UpdateGroupDetailsCommandHandler(
        IQueryHandler<UserByIdQuery, User> userByIdQueryHandler,
        IQueryHandler<UserGroupQuery, Group> userGroupQueryHandler,
        IVersionedRepository<Group, uint> groupsRepository)
    {
        // ...
    }

    public async Task<UpdateGroupDetailsCommandResult> Handle(
        UpdateGroupDetailsCommand request,
        CancellationToken cancellationToken)
    {
        var group = await _userGroupQueryHandler.HandleAsync(
            new UserGroupQuery(request.UserId, request.GroupId),
            cancellationToken);

        if (group is null)
        {
            return null;
        }

        var currentUser = await _userByIdQueryHandler.HandleAsync(
            new UserByIdQuery(request.UserId),
            cancellationToken);

        group.Rename(currentUser, request.Name);

        await _groupsRepository.UpdateAsync(
            group,
            uint.Parse(request.RowVersion),
            cancellationToken);

        return new UpdateGroupDetailsCommandResult(
            group.Id,
            group.Name,
            group.RowVersion.ToString());
    }
}

In general, the code is the same as it was before, with a small change. We're fetching the group and current user information, then we call group.Rename, without needing any additional logic, as it's present in the Rename method. The rest, again, the same as before, update the data in the database and then return the updated group.

Playing nicely with EF Core

Until now, we were focusing on the domain logic part of our application. Now we need to be able to persist said entities to the database.

In a perfect world, we could completely design the entities (and all the domain logic for that matter) without concerning ourselves with what goes on in the infrastructure side of things. In reality, there are decisions to make and tradeoffs to consider. If we want a completely "pure" domain, we'll have more work in the infrastructure, as we'd probably need to replicate all the entity classes to use for persistence (usually referred to as the persistence model). Even then, we'd need to have a way to create the domain entities given the persistence model, as in a "pure" implementation, it wouldn't normally expose all of its internal data for others to set.

Fortunately, EF Core (and other ORMs) lets us get away with just a few tweaks, so we won't have a complete infrastructure agnostic domain model, but it won't be too far off.

Going back to the group entity we saw above, one thing I omitted is the presence of a private parameterless constructor. Let me drop here the relevant code for this section.

Group.cs

public class Group : IVersionedEntity
{
    private readonly List<GroupUser> _groupUsers = new List<GroupUser>();

    public Group(string name, User creator)
    {
        // ...
    }

    private Group()
    {
    }

    public long Id { get; private set; }
    public string Name { get; private set; }
    public uint RowVersion { get; private set; }
    public User Creator { get; private set; }
    public IReadOnlyCollection<GroupUser> GroupUsers => _groupUsers.AsReadOnly();

    // ...
}

EF Core can use constructors with parameters, as long as they're not navigation properties (maybe this changes in the future). In this case, the creator parameter is a navigation property, so we're out of luck with it. Alternatively, by having a parameterless constructor, EF can create an instance of the class (even if it is private, reflection FTW 😛).

With an instance of the object in hand, EF sets the various properties. That's the reason (almost) all properties have a private setter, even things that don't change, like the id, that could get away with having a getter only. By having a private setter, much like the private constructor, EF can use it to hydrate the entity with the data from the database.

As for the GroupUsers property, EF can't set it directly, but it can figure out it has a backing field and work with it, being automatic as long as we follow some conventions, in this case, the backing field name is the camel case version of the property, prefixed with an underscore. If we didn't follow such conventions, we could configure it in the IEntityTypeConfiguration implementations we have in the infrastructure project, but as we are, no need for that.

As the entities we have so far aren't very complex, that's all there is to it, but EF Core has more features like these to ease the work with rich domain models, so it's a matter of investigating the possibilities as needs arise.

Outro

That's about it for this episode, as we made a code a bit more object oriented, by moving the logic to the place it makes sense to be, instead of shoving everything in services (or request handlers).

Although the focus was on better separation of concerns, through object oriented practices, with it some common DDD related topics emerged. If you're looking for specific DDD resources, Julie Lerman, Steve Smith and Vladimir Khorikov blogs/courses are a good bet.

Links in the post:

The source code for this post is in the GroupManagement repository, tagged as episode036.

Sharing and feedback always appreciated!

Thanks for stopping by, cyaz!

ASP.NET Core: From 0 to overkill (45 Part Series)

1) ASP.NET Core: From 0 to overkill - Intro 2) Episode 001 - The Reference Project - ASP.NET Core: From 0 to overkill 3 ... 43 3) Episode 002 - Project structure plus first application - ASP.NET Core: From 0 to overkill 4) Episode 003 - First steps with MVC - ASP.NET Core: From 0 to overkill 5) Episode 004 - The Program and Startup classes - ASP.NET Core: From 0 to overkill 6) Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill 7) Episode 006 - Configuration - ASP.NET Core: From 0 to overkill 8) Episode 007 - Logging - ASP.NET Core: From 0 to overkill 9) Episode 008 - Middlewares - ASP.NET Core: From 0 to overkill 10) Episode 009 - MVC filters - ASP.NET Core: From 0 to overkill 11) Episode 010 - Async all the things - ASP.NET Core: From 0 to overkill 12) Episode 011 - Data access with Entity Framework Core - ASP.NET Core: From 0 to overkill 13) Episode 012 - Move to a Web API - ASP.NET Core: From 0 to overkill 14) Episode 013 - Starting the frontend with Vue.js - ASP.NET Core: From 0 to overkill 15) Episode 014 - Centralizing frontend state with Vuex - ASP.NET Core: From 0 to overkill 16) Episode 015 - Calling the Web API from the frontend - ASP.NET Core: From 0 to overkill 17) Episode 016 - Authentication with Identity and Razor Pages - ASP.NET Core: From 0 to overkill 18) Episode 017 - More Identity, more Razor Pages - ASP.NET Core: From 0 to overkill 19) Episode 018 - Internationalization - ASP.NET Core: From 0 to overkill 20) Episode 019 - Roles, claims and policies - ASP.NET Core: From 0 to overkill 21) Episode 020 - The backend for frontend and the HttpClient - ASP.NET Core: From 0 to overkill 22) Episode 021 - Integrating IdentityServer4 - Part 1 - Overview - ASP.NET Core: From 0 to overkill 23) Episode 022 - Integrating IdentityServer4 - Part 2 - Auth Service - ASP.NET Core: From 0 to overkill 24) Episode 023 - Integrating IdentityServer4 - Part 3 - API - ASP.NET Core: From 0 to overkill 25) Episode 024 - Integrating IdentityServer4 - Part 4 - Back for Front - ASP.NET Core: From 0 to overkill 26) Episode 025 - Integrating IdentityServer4 - Part 5 - Frontend - ASP.NET Core: From 0 to overkill 27) Episode 026 - Getting started with Docker - ASP.NET Core: From 0 to overkill 28) Episode 027 - Up and running with Docker Compose - ASP.NET Core: From 0 to overkill 29) Episode 028 - Multiple service instances tweaks - ASP.NET Core: From 0 to overkill 30) Episode 029 - Simplifying the BFF with ProxyKit - ASP.NET Core: From 0 to overkill 31) Episode 030 - Analyzing performance with BenchmarkDotNet - ASP.NET Core: From 0 to overkill 32) Episode 031 - Some simple unit tests with xUnit - ASP.NET Core: From 0 to overkill 33) Episode 032 - Upgrading to ASP.NET Core 3.0 - ASP.NET Core: From 0 to overkill 34) E033 - Redesigning the API: Improving the internal architecture - ASPF02O 35) E034 - Segregating use cases with MediatR - ASPF02O 36) E035 - Experimenting with (yet) another approach to data access organization - ASPF02O 37) E036 - Making things more object oriented with rich domain entities - ASPF02O 38) Better use of types - avoiding nulls with an Optional type - ASPF02O|E037 39) More explicit domain error handling and fewer exceptions with Either and Error types [ASPF02O|E038] 40) Event-driven integration - Overview [ASPF02O|E039] 41) Event-driven integration #1 - Intro to the transactional outbox pattern [ASPF02O|E040] 42) Event-driven integration #2 - Inferring events from EF Core changes [ASPF02O|E041] 43) Event-driven integration #3 - Storing events in the outbox table [ASPF02O|E042] 44) Event-driven integration #4 - Outbox publisher (feat. IHostedService & Channels) [ASPF02O|E043] 45) Event-driven integration #5 - Quick intro to Apache Kafka [ASPF02O|E044]

Posted on Oct 27 '18 by:

Discussion

markdown guide
 

hello joao <3. hope you're doing great. im back to sync with all your posts, one by one :) enshallah. this article, although brief, but it helped. thank you bro.