40

Domain Model Encapsulation and PI with Entity Framework 2.2

 5 years ago
source link: https://www.tuicool.com/articles/hit/JBF77jA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction

Inprevious 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:

VjmyUjq.png!web

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:

3AF3q2y.png!web

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:

Clean Architecture

Onion Architecture

Ports And Adapters / Hexagonal Architecture

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 that each architecture Business Logic/Business Layer/Entities/Domain Layer 1) is in the center and 2) has no dependency to other components/layers/modules. It is the same in my example:

2e2QZjz.png!web

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:

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);
    }
}
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 :

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;
    }
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:

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:

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.

public enum OrderStatus
{
    Placed = 0,
    InRealization = 1,
    Canceled = 2,
    Delivered = 3,
    Sent = 4,
    WaitingForPayment = 5
}
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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK