清洁架构 EF Core DbContext 项目

Clean Arcitecture EF Core DbContext project

正在寻找有关 Clean Architecture 方法和 EF Core 的一些一般指导。

我有一个 Web 应用程序 (UI) 项目、Domain/Business 项目和我的 Data/Infrastructure 项目用于所有 crud 操作。

引入和创建 DbContext 时,上下文本身应该存储在数据项目中,而实体 (table 类) 应该存储在 Domain/Business 项目中,因为它们有没有直接的 crud 选项。

或者 DbContext 和相应的实体 (tables) 应该一起在 Data 项目中。

感谢任何帮助。

您应该根据业务需求设计您的域模型类,并以最有效的方式实现业务域不变量。

这意味着 不要对您的域模型实体造成任何基础架构问题的负担。如果将这些 类 放入域项目中,将 DbContext 和 EFCore 配置 类 之类的东西放入数据层,则可以更容易地遵守此原则。

您可以查看参考资料 Microsoft reference project,其中采用了这种方法。

放置在核心(或业务领域)项目中的order entity(这里也是一个聚合根)除了无参数私有构造函数之外没有任何基础设施问题EFCore 要求 - 可接受的权衡。

using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using System;
using System.Collections.Generic;

namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate
{
    public class Order : BaseEntity, IAggregateRoot
    {
        private Order()
        {
            // required by EF
        }

        public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
        {
            Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
            Guard.Against.Null(shipToAddress, nameof(shipToAddress));
            Guard.Against.Null(items, nameof(items));

            BuyerId = buyerId;
            ShipToAddress = shipToAddress;
            _orderItems = items;
        }

        public string BuyerId { get; private set; }
        public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
        public Address ShipToAddress { get; private set; }

        // DDD Patterns comment
        // Using a private collection field, better for DDD Aggregate's encapsulation
        // so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection,
        // but only through the method Order.AddOrderItem() which includes behavior.
        private readonly List<OrderItem> _orderItems = new List<OrderItem>();

        // Using List<>.AsReadOnly() 
        // This will create a read only wrapper around the private list so is protected against "external updates".
        // It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance)
        //https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx 
        public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

        public decimal Total()
        {
            var total = 0m;
            foreach (var item in _orderItems)
            {
                total += item.UnitPrice * item.Units;
            }
            return total;
        }
    }
}

order entity database configuration 被放置在 infrastructure/data 项目中,以检测域和数据库层之间的映射,使域项目独立于此类基础设施问题。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;

namespace Microsoft.eShopWeb.Infrastructure.Data.Config
{
    public class OrderConfiguration : IEntityTypeConfiguration<Order>
    {
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));

            navigation.SetPropertyAccessMode(PropertyAccessMode.Field);

            builder.OwnsOne(o => o.ShipToAddress, a =>
            {
                a.WithOwner();
                
                a.Property(a => a.ZipCode)
                    .HasMaxLength(18)
                    .IsRequired();

                a.Property(a => a.Street)
                    .HasMaxLength(180)
                    .IsRequired();

                a.Property(a => a.State)
                    .HasMaxLength(60);

                a.Property(a => a.Country)
                    .HasMaxLength(90)
                    .IsRequired();

                a.Property(a => a.City)
                    .HasMaxLength(100)
                    .IsRequired();
            });
        }
    }
}

有了这个,您就可以清楚地分离关注点,可以专注于业务层中的业务逻辑,获得更好的逻辑可测试性,并且您仍然能够利用 EFCore 的优势。