使用EF的领域模型的封装和持久化透明(PI)

翻译原文地址

系列文章目录

一、简单的CQRS实现与原始SQL和DDD
二、使用EF的领域模型的封装和持久化透明(PI)
三、REST API数据验证
四、领域模型验证
五、如何发布和处理领域事件
六、处理领域事件:缺失的部分
七、发件箱模式(The Outbox Pattern)
八、.NET Core中的旁路缓存模式(Cache-Aside Pattern)

引言

在之前的文章中,我介绍了如何使用原始SQL(读模型)和领域驱动设计(写模型)实现简单的CQRS模式。我想继续介绍主要集中在DDD实施的例子。在这篇文章中,我将描述如何尽可能多地利用最新版本的Entity Framework 2.2(译者注:现在已不是最新版本)来支持纯领域建模。

我决定在GitHub上不断开发我的例子。我将尝试逐步添加新的功能和技术解决方案。我还将尝试扩展领域,使应用程序与实际应用程序相似。在琐碎的领域中很难解释DDD的某些方面。不过,我强烈建议您关注(follow)我的代码库。

目标

当我们创建领域模型时,必须考虑很多事情。在这一点上,我想聚焦于其中的两个:封装(Encapsulation)和持久化透明(Persistence Ignorance)。

封装(Encapsulation

封装有两个主要的定义(来源-维基百科):

一种语言机制,用于限制直接访问对象的某些组件

以及

一种便于将数据与操作该数据的方法(或其他函数)绑定的语言结构

这对DDD聚合意味着什么?这仅仅意味着我们应该对外部世界隐藏我们聚合的所有内部信息。理想情况下,我们应该只公开满足业务需求所需的公共方法。

设想如下:

image

持久化透明(Persistence Ignorance)

持久化透明(Persistence Ignorance, PI)原则认为,领域模型应该不知道它的数据是如何保存或检索的。这是值得遵循的非常好的和重要的建议。然而,我们应该谨慎地遵循这个建议。我同意微软文档中的观点:

即使领域模型遵循PI原则非常重要,也不应该忽略持久化(persistence)问题。理解物理数据模型以及它如何映射到实体对象模型仍然非常重要。否则你就会创造出不可能的设计。

如上所述,很不幸,我们不能忘记持久化。尽管如此,我们应该尽可能地将领域模型与系统的其他部分分离。

示例领域

为了更好地理解所创建的领域模型,我准备了下面的图:

image

它是简单的电子商务领域。客户(Customer)可以下一个或多个订单(Orders)。订单是一组具有数量信息的产品 (ProductOrder)。每个产品(Product)都根据货币(Currency)定义了许多价格(ProductPrice)。

好了,我们知道了问题所在,现在我们可以开始转到解决方案部分了…

解决方案

1. 创建支持架构

首先,也是最重要的事情是创建应用程序架构,它同时支持领域模型的封装和持久化透明。最常见的例子有:

  • 整洁架构(Clean Architecture
  • 洋葱架构(Onion Architecture
  • 端口和适配器/六边形架构(Ports And Adapters / Hexagonal Architecture

所有这些体系结构在生产系统中都很好。对我来说,整洁架构和洋葱架构几乎是一样的。端口和适配器/六边形架构在命名方面略有不同,但一般原则是相同的。

在领域建模上下文中最重要的事情是,每个架构业务逻辑/业务层/实体/领域层 位于中心,不依赖于其他组件/层/模块。在我的例子中也是一样的:

image

这在实践中对我们在领域模型中的代码意味着什么?

  1. 没有数据访问代码。
  2. 我们的实体没有数据标记(annotations)。
  3. 不继承任何框架类,实体应该是普通旧CLR对象(Plain Old CLR Object

2. 只在基础设施层使用实体框架

任何与数据库的交互都应该在基础设施层实现。这意味着你必须添加实体框架上下文、实体映射和存储库的实现。领域模型中只能保存存储库的接口。

3. 使用影子属性(Shadow Properties)

影子属性是将实体与数据库模式解耦的好方法。它们是仅在EF模型中定义的属性。使用它们,我们通常不需要在领域模型中包含外键,这是一件很棒的事情。

让我们看看订单(Order)实体及其在CustomerEntityTypeConfiguration映射中定义的映射:


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

正如您在第15行所看到的,我们正在定义Order实体中不存在的属性。它仅为Customer和Order之间的关系配置而定义。Order和ProductOrder关系也是如此.

4. 使用自有实体类型(Owned Entity Types)

使用自有实体类型,我们可以创建更好的封装,因为我们可以直接映射到私有或内部字段:

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

自由类型也是创建值对象的很好的解决方案。这是MoneyValue的样子:

public class MoneyValue
{
    public decimal Value { get; }

    public string Currency { get; }

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

5. 映射到私有字段

我们不仅可以使用EF拥有的类型映射到私有字段,还可以映射到内置类型。我们所要做的就是给出字段和列的名称:

x.Property<bool>("_isRemoved").HasColumnName("IsRemoved");

6. 使用值转换

值转换是实体属性和表列值之间的“桥梁”。如果类型之间不兼容,我们应该使用它们。EF有很多开箱即用的值转换器。此外,如果我们需要,我们可以实现自定义转换器。


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>());

这个转换器简单地将“StatusId”列字节类型转换为OrderStatus类型的私有字段_status。

总结

在这篇文章中,我简要地描述了什么是封装和持久化透明(在领域建模的上下文中),以及我们如何通过以下方式实现这些方法:

  • 创建支持架构
  • 将所有数据访问代码放在领域模型实现之外
  • 使用EF功能:影子属性,自有实体类型,私有字段映射,值转换
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值