Loading...
Skip to Content

Blog

Post: Domain Model Encapsulation and PI with Entity Framework 2.2

Domain Model Encapsulation and PI with Entity Framework 2.2

Introduction

In previous post I presented how to implement simple CQRS pattern using raw SQL (Read Model) and Domain Driven Design (Write Model). I would like to continue presented example focusing mainly on DDD implementation. In this post I will describe how to get most out of the newest version Entity Framework v 2.2 to support pure domain modeling as much as possible.

I decided that I will constantly develop my sample on GitHub. I will try to gradually add new functionalities and technical solutions. I will also try to extend domain so that the application will become similar to the real ones. It is difficult to explain some DDD aspects on trivial domains. Nevertheless, I highly encourage you to follow my codebase.

Goals

When we create our Domain Model we have to take many things into account. At this point I would like to focus on 2 of them: Encapsulation and Persistence Ignorance.

Encapsulation

Encapsulation has two major definitions (source - Wikipedia):

A language mechanism for restricting direct access to some of the object’s components

and

A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data

What does it mean to DDD Aggregates? It just simply mean that we should hide all internals of our Aggregate from the outside world. Ideally, we should expose only public methods which are required to fulfill our business requirements. This assumption is presented below:

Encapsulation

Persistence Ignorance

Persistence Ignorance (PI) principle says that the Domain Model should be ignorant of how its data is saved or retrieved. It is very good and important advice to follow. However, we should follow it with caution. I agree with opinion presented in the Microsoft documentation:

Even when it is important to follow the Persistence Ignorance principle for your Domain model, you should not ignore persistence concerns. It is still very important to understand the physical data model and how it maps to your entity object model. Otherwise you can create impossible designs.

As described, we can’t forget about persistence, unfortunately. Nevertheless, we should aim at decoupling _Domain Model from rest parts of our system as much as possible.

Example Domain

For a better understanding of the created Domain Model I prepared the following diagram:

Orders Context

It is simple e-commerce domain. Customer can place one or more Orders. Order is a set of Products with information of quantity (OrderProduct). Each Product has defined many prices (ProductPrice) depending on the Currency.

Ok, we know the problem, now we can go to the solution…

Solution

1. Create supporting architecture

First and most important thing to do is create application architecture which supports both Encapsulation and Persistence Ignorance of our Domain Model. The most common examples are:

All of these architectures are good and and used in production systems. For me Clean Architecture and Onion Architecture are almost the same. Ports And Adapters / Hexagonal Architecture is a little bit different when it comes to naming, but general principles are the same. The most important thing in context of domain modeling is:

  1. each architecture has Business Logic/Business Layer/Entities/Domain Layer in the center and
  2. this center does not have dependencies to other components/layers/modules.

It is the same in my example:

Solution dependencies

What this means in practice for our code in Domain Model?

  1. No data access code.
  2. No data annotations for our entities.
  3. No inheritance from any framework classes, entities should be Plain Old CLR Object

2. Use Entity Framework in Infrastructure Layer only

Any interaction with database should be implemented in Infrastructure Layer. It means you have to add there entity framework context, entity mappings and implementation of repositories. Only interfaces of repositories can be kept in Domain Model.

3. Use Shadow Properties

Shadow Properties are great way to decouple our entities from database schema. They are properties which are defined only in Entity Framework Model. Using them we often don’t need to include foreign keys in our Domain Model and it is great thing.

Let’s see the Order Entity and its mapping which is defined in CustomerEntityTypeConfiguration mapping:

// Order entity
public class Order : Entity
{
    internal Guid Id;
    private bool _isRemoved;
    private MoneyValue _value;
    private List<OrderProduct> _orderProducts;

    private Order()
    {
        this._orderProducts = new List<OrderProduct>();
        this._isRemoved = false;
    }

    public Order(List<OrderProduct> orderProducts)
    {
        this.Id = Guid.NewGuid();
        this._orderProducts = orderProducts;

        this.CalculateOrderValue();
    }

    internal void Change(List<OrderProduct> orderProducts)
    {
        foreach (var orderProduct in orderProducts)
        {
            var existingOrderProduct = this._orderProducts.SingleOrDefault(x => x.Product == orderProduct.Product);
            if (existingOrderProduct != null)
            {
                existingOrderProduct.ChangeQuantity(orderProduct.Quantity);
            }
            else
            {
                this._orderProducts.Add(orderProduct);
            }
        }

        var existingProducts = this._orderProducts.ToList();
        foreach (var existingProduct in existingProducts)
        {
            var product = orderProducts.SingleOrDefault(x => x.Product == existingProduct.Product);
            if (product == null)
            {
                this._orderProducts.Remove(existingProduct);
            }
        }

        this.CalculateOrderValue();
    }

    internal void Remove()
    {
        this._isRemoved = true;
    }

    private void CalculateOrderValue()
    {
        var value = this._orderProducts.Sum(x => x.Value.Value);
        this._value = new MoneyValue(value, this._orderProducts.First().Value.Currency);
    }
}
// CustomerEntityTypeConfiguration class
internal class CustomerEntityTypeConfiguration : IEntityTypeConfiguration<Customer>
{
    internal const string OrdersList = "_orders";
    internal const string OrderProducts = "_orderProducts";

    public void Configure(EntityTypeBuilder<Customer> builder)
    {
        builder.ToTable("Customers", SchemaNames.Orders);
        
        builder.HasKey(b => b.Id);
        
        builder.OwnsMany<Order>(OrdersList, x =>
        {
            x.ToTable("Orders", SchemaNames.Orders);
            x.HasForeignKey("CustomerId"); // Shadow property
            x.Property<bool>("_isRemoved").HasColumnName("IsRemoved");
            x.Property<Guid>("Id");
            x.HasKey("Id");

            x.OwnsMany<OrderProduct>(OrderProducts, y =>
            {
                y.ToTable("OrderProducts", SchemaNames.Orders);
                y.Property<Guid>("OrderId"); // Shadow property
                y.Property<Guid>("ProductId"); // Shadow property
                y.HasForeignKey("OrderId");
                y.HasKey("OrderId", "ProductId");

                y.HasOne(p => p.Product);

                y.OwnsOne<MoneyValue>("Value", mv =>
                {
                    mv.Property(p => p.Currency).HasColumnName("Currency");
                    mv.Property(p => p.Value).HasColumnName("Value");
                });
            });

            x.OwnsOne<MoneyValue>("_value", y =>
            {
                y.Property(p => p.Currency).HasColumnName("Currency");
                y.Property(p => p.Value).HasColumnName("Value");
            });
        });
    }
}

As you can see on line 15 we are defining property which doesn’t exist in Order entity. It is defined only for relationship configuration between Customerand Order. The same is for Order and ProductOrder relationship (see lines 23, 24).

4. Use Owned Entity Types

Using Owned Entity Types we can create better encapsulation because we can map directly to private or internal fields:

// Order entity part
public class Order : Entity
{
    internal Guid Id;
    private bool _isRemoved;
    private MoneyValue _value;
    private List<OrderProduct> _orderProducts;

    private Order()
    {
        this._orderProducts = new List<OrderProduct>();
        this._isRemoved = false;
    }
// OwnsMany and OwnsOne
x.OwnsMany<OrderProduct>(OrderProducts, y =>
{
    y.ToTable("OrderProducts", SchemaNames.Orders);
    y.Property<Guid>("OrderId"); // Shadow property
    y.Property<Guid>("ProductId"); // Shadow property
    y.HasForeignKey("OrderId");
    y.HasKey("OrderId", "ProductId");

    y.HasOne(p => p.Product);

    y.OwnsOne<MoneyValue>("Value", mv =>
    {
        mv.Property(p => p.Currency).HasColumnName("Currency");
        mv.Property(p => p.Value).HasColumnName("Value");
    });
});

x.OwnsOne<MoneyValue>("_value", y =>
{
    y.Property(p => p.Currency).HasColumnName("Currency");
    y.Property(p => p.Value).HasColumnName("Value");
});

Owned types are great solution for creating our Value Objects too. This is how MoneyValue looks like:

// MoneyValue ValueObject
public class MoneyValue
{
    public decimal Value { get; }

    public string Currency { get; }

    public MoneyValue(decimal value, string currency)
    {
        this.Value = value;
        this.Currency = currency;
    }
}

5. Map to private fields

We can map to private fields not only using EF owned types, we can map to built-in types too. All we have to do is give the name of the field and column:

// Mapping to private built-in type
x.Property<bool>("_isRemoved").HasColumnName("IsRemoved");

6. Use Value Conversions

Value Conversions are the “bridge” between entity attributes and table column values. If we have incompatibility between types, we should use them. Entity Framework has a lot of value converters implemented out of the box. Additionally, we can implement custom converter if we need to.

// OrderStatus
public enum OrderStatus
{
    Placed = 0,
    InRealization = 1,
    Canceled = 2,
    Delivered = 3,
    Sent = 4,
    WaitingForPayment = 5
}
// Value Conversion
x.Property("_status").HasColumnName("StatusId").HasConversion(new EnumToNumberConverter<OrderStatus, byte>());

This converter simply converts “StatusId” column byte type to private field _status of type OrderStatus.

Summary

In this post I described shortly what Encapsulation and Persistence Ignorance is (in context of domain modeling) and how we can achieve these approaches by:

  • creating supporting architecture
  • putting all data access code outside our domain model implementation
  • using Entity Framework Core features: Shadow Properties, Owned Entity Types, private fields mapping, Value Conversions

Comments

Related posts See all blog posts

Attributes of Clean Domain Model
28 October 2019
There is a lot of talk about clean code and architecture nowadays. There is more and more talk about how to achieve it. The rules described by Robert C. Martin are universal and in my opinion, we can use them in various other contexts. In this post I would like to refer them to the context of the Domain Model implementation, which is often the heart of our system. We want to have a clean heart, aren't we?
Read More
Domain Model Validation
4 March 2019
In previous post I described how requests input data can be validated on Application Services Layer. I showed FluentValidation library usage in combination with Pipeline Pattern and Problem Details standard. In this post I would like to focus on the second type of validation which sits in the Domain Layer – Domain Model validation.
Read More
Simple CQRS implementation with raw SQL and DDD
4 February 2019
In this post I wanted to show you how you can quickly implement simple REST API application with CQRS using the .NET Core.
Read More