Blog
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:
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:
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:
- each architecture has Business Logic/Business Layer/Entities/Domain Layer in the center and
- this center does not have dependencies to other components/layers/modules.
It is the same in my example:
What this means in practice for our code in Domain Model?
- No data access code.
- No data annotations for our entities.
- 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 Customer
and 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