.NET Core DDD实现指南|分层架构设计与实战案例

.NET Core DDD实现指南|分层架构设计与实战案例 一

文章目录CloseOpen

分层架构核心逻辑:从理论到.NET Core落地

很多人觉得DDD是”高大上”的理论,其实它最实在的价值就是帮你把”做什么”(业务)和”怎么做”(技术)分开。就像盖房子,先画好户型图(业务模型),再考虑用什么砖(技术框架)。在.NET Core里实现DDD分层架构是基础,这四层你得搞明白:

领域层:业务逻辑的”心脏”

这层是DDD的灵魂,专门放业务规则和领域模型。你可以把它理解成”只说业务话”的地方——比如订单怎么创建、商品能不能退款,这些规则都写在这里。我见过不少项目把业务逻辑塞在Service里,结果技术框架一换(比如从EF Core换成Dapper),业务代码跟着改,这就是没把领域层拎清楚。

在.NET Core里,领域层通常是个类库项目,里面主要有:

  • 实体(Entity):有唯一ID、有生命周期的业务对象,比如订单、商品
  • 值对象(Value Object):没独立生命周期、靠属性值判断相等的对象,比如地址、金额(100元+20元运费和120元总金额是同一个值对象)
  • 聚合根(Aggregate Root):管理一组实体和值对象的”负责人”,比如订单就是聚合根,订单项、收货地址都归它管,对外只能通过订单操作这些对象,保证数据一致性
  • 举个代码例子,电商订单的实体类应该这样写(重点看领域方法,把业务规则封装在里面):

    // 领域层 
  • 实体
  • public class Order AggregateRoot

    {

    public Guid Id { get; private set; } // 聚合根ID

    public string OrderNo { get; private set; } // 订单编号

    public Address ShippingAddress { get; private set; } // 值对象:收货地址

    public List Items { get; private set; } = new(); // 订单项集合

    public OrderStatus Status { get; private set; } = OrderStatus.Draft; // 订单状态

    // 领域方法:添加订单项(封装业务规则)

    public void AddItem(Product product, int quantity)

    {

    if (Status != OrderStatus.Draft)

    throw new DomainException("只有草稿状态的订单能添加商品");

    if (quantity <= 0)

    throw new DomainException("商品数量必须大于0");

    Items.Add(new OrderItem(

    productId: product.Id,

    productName: product.Name,

    unitPrice: product.Price,

    quantity: quantity

    ));

    }

    }

    应用层:”业务协调员”而非”业务执行者”

    应用层是领域层和外部的”中间人”,负责协调领域对象完成业务流程,但不包含业务规则。比如”创建订单”这个用例,应用层会调用订单实体的AddItem方法,再调用仓储保存订单,至于”为什么只能草稿状态添加商品”这种规则,它不管——那是领域层的事。

    我之前见过有人把业务逻辑全堆在ApplicationService里,结果领域层成了空壳子。其实应用层就像餐厅的前台,顾客点单(用户请求)后,它告诉后厨(领域层)做什么菜,再把做好的菜端给顾客,自己不做菜。

    基础设施层:技术细节的”收容所”

    数据库访问、缓存、消息队列这些技术相关的代码,都放这里。比如用EF Core实现仓储接口、用Redis缓存商品数据。关键是要依赖注入,让领域层只依赖接口,不依赖具体技术——这样以后想把SQL Server换成PostgreSQL,改基础设施层就行,不用动领域层代码。

    表现层:用户交互的”窗口”

    API控制器、页面这些直接和用户打交道的部分,负责接收请求、返回响应。这里要注意别把业务逻辑放进来,我见过控制器里写订单计算逻辑的,结果移动端和PC端各有一套,改起来要改两处,血的教训。

    为了让你更清楚各层职责,我整理了一个.NET Core项目结构表,这是我重构项目时用的,亲测好用:

    分层 项目类型 核心职责 依赖方向
    领域层 Class Library 业务规则、领域模型、领域服务 不依赖其他层
    应用层 Class Library 协调领域对象、编排业务流程 依赖领域层
    基础设施层 Class Library 数据库访问、缓存、第三方服务集成 依赖领域层、应用层
    表现层 ASP.NET Core Web API 接收请求、返回响应、权限验证 依赖应用层、基础设施层

    实战案例:电商订单系统DDD实现全流程

    光说理论太空泛,咱们拿电商订单系统举例子——这是我做过三个项目都遇到的场景,用DDD实现特别合适。下面从领域模型设计到代码落地,一步一步带你走。

    第一步:领域模型设计(核心中的核心)

    先别急着写代码,拿张纸画业务流程图——订单从创建到支付、发货、完成,有哪些状态?涉及哪些对象?我当时和产品经理掰扯了两天,才把”订单取消后能不能恢复”、”商品下架后未支付订单怎么处理”这些规则定下来,这一步省了后面无数返工。

    订单聚合根设计

    :订单是聚合根,包含订单项(值对象)、收货地址(值对象),聚合根要保证内部数据一致性。比如取消订单时,所有订单项状态也要同步更新,不能有”订单已取消但订单项还在待发货”的情况。 值对象设计:收货地址(省、市、区、街道、门牌号)、金额(数值、币种)都是值对象。值对象的关键是”按值相等”,比如两个地址只要详细信息一样,就认为是同一个地址,不用像实体那样靠ID区分。在.NET里可以重写Equals方法实现:

    // 值对象:地址
    

    public class Address ValueObject

    {

    public string Province { get; }

    public string City { get; }

    public string District { get; }

    public string Detail { get; }

    public Address(string province, string city, string district, string detail)

    {

    // 校验逻辑(省略)

    Province = province;

    City = city;

    District = district;

    Detail = detail;

    }

    // 重写Equals:按属性值判断相等

    protected override IEnumerable GetEqualityComponents()

    {

    yield return Province;

    yield return City;

    yield return District;

    yield return Detail;

    }

    }

    领域事件设计

    :订单状态变更时(比如支付成功),要通知库存扣减、积分增加,这时候用领域事件解耦。在.NET Core里可以用MediatR库实现事件发布/订阅,领域层只定义事件,基础设施层实现事件处理:

    // 领域事件:订单支付成功
    

    public class OrderPaidEvent DomainEvent

    {

    public Guid OrderId { get; }

    public decimal Amount { get; }

    public OrderPaidEvent(Guid orderId, decimal amount)

    {

    OrderId = orderId;

    Amount = amount;

    }

    }

    // 订单实体中发布事件

    public void Pay(decimal actualPayment)

    {

    Status = OrderStatus.Paid;

    AddDomainEvent(new OrderPaidEvent(Id, actualPayment)); // 发布事件

    }

    第二步:仓储模式实现(数据持久化)

    领域层定义仓储接口,基础设施层用EF Core实现。仓储接口只包含领域需要的方法,比如”根据订单号查询订单”、”保存订单”,别把”分页查询所有订单”这种技术相关的方法放进去——那是应用层的事。

    // 领域层:仓储接口
    

    public interface IOrderRepository IRepository

    {

    Task GetByOrderNoAsync(string orderNo);

    Task ExistsByProductIdAsync(Guid productId);

    }

    // 基础设施层:EF Core实现

    public class OrderRepository EfCoreRepository, IOrderRepository

    {

    private readonly DbContext _dbContext;

    public OrderRepository(DbContext dbContext) base(dbContext)

    {

    _dbContext = dbContext;

    }

    public async Task GetByOrderNoAsync(string orderNo)

    {

    return await _dbContext.Set()

    .Include(o => o.Items)

    .FirstOrDefaultAsync(o => o.OrderNo == orderNo);

    }

    }

    第三步:应用层服务编排

    应用层实现”创建订单”用例:接收前端传来的商品列表、收货地址,调用订单实体的AddItem方法添加商品,校验库存(调用库存领域服务),最后调用仓储保存订单。代码示例:

    // 应用层服务
    

    public class OrderAppService IOrderAppService

    {

    private readonly IOrderRepository _orderRepository;

    private readonly IInventoryService _inventoryService; // 领域服务

    private readonly IUnitOfWork _unitOfWork;

    public OrderAppService(IOrderRepository orderRepository,

    IInventoryService inventoryService,

    IUnitOfWork unitOfWork)

    {

    _orderRepository = orderRepository;

    _inventoryService = inventoryService;

    _unitOfWork = unitOfWork;

    }

    // 创建订单

    public async Task CreateOrder(CreateOrderCommand command)

    {

    //

  • 校验库存
  • foreach (var item in command.Items)

    {

    var hasStock = await _inventoryService.CheckStockAsync(item.ProductId, item.Quantity);

    if (!hasStock)

    throw new ApplicationException($"商品{item.ProductName}库存不足");

    }

    //

  • 创建订单实体(调用领域方法)
  • var order = new Order(

    orderNo: GenerateOrderNo(),

    shippingAddress: new Address(command.Province, command.City, command.District, command.Detail)

    );

    foreach (var item in command.Items)

    {

    order.AddItem(new Product(item.ProductId, item.ProductName, item.Price), item.Quantity);

    }

    //

  • 保存订单(事务控制)
  • await _orderRepository.AddAsync(order);

    await _unitOfWork.SaveChangesAsync();

    //

  • 返回DTO
  • return new OrderDto

    {

    OrderId = order.Id,

    OrderNo = order.OrderNo,

    TotalAmount = order.GetTotalAmount()

    };

    }

    }

    第四步:依赖注入配置(.NET Core的强项)

    在Program.cs里配置各层依赖注入,让应用层依赖仓储接口,基础设施层提供实现,这样领域层完全不依赖具体技术:

    // 依赖注入配置
    

    builder.Services.AddScoped();

    builder.Services.AddScoped();

    builder.Services.AddScoped(sp => sp.GetRequiredService()); // 工作单元

    踩坑提醒

    :我第一次做的时候,把仓储实现直接放领域层了,结果换数据库时改了一堆领域代码。后来才明白——领域层只定义”要做什么”(接口),基础设施层告诉”怎么做”(实现),这才是依赖倒置的精髓。

    按照这个流程做下来,你会发现业务逻辑全在领域层,应用层和表现层很薄,改需求时基本不用动核心业务代码。我那个电商项目后来加了”预售订单”功能,只新增了一个PresaleOrder实体和对应的应用服务,老代码一行没改,上线那天我准时下班了——这在以前想都不敢想。

    如果你手头有.NET项目觉得难维护,不妨试试用DDD分层架构梳理一遍,先从画业务流程图开始,别怕慢,这一步走稳了,后面写代码会像开了挂一样顺。要是在领域模型设计或分层上卡住了,欢迎在评论区留言,我帮你看看怎么优化。


    在.NET Core里搞DDD的四层架构,可不是随便把代码分到四个文件夹就完事了,这里面藏着个“依赖倒置”的规矩,你得把这个理顺了,后面写代码才不会乱。简单说就是:上层不用管下层具体怎么干,只要知道下层能提供什么功能就行——就像你点外卖,不用知道厨师怎么炒菜(具体实现),只要知道餐厅能做宫保鸡丁(抽象接口)就行。

    具体到四层的依赖关系,你可以这么记:最核心的是领域层,它就像个“甩手掌柜”,只负责定业务规矩(比如订单怎么创建、商品能不能退款),不依赖任何其他层,谁都别想指挥它;然后是应用层,它是领域层的“小助理”,专门协调领域对象干活,但它只听领域层的,别的层想指挥它?门儿都没有;再往下是基础设施层,这层就像“技术打工仔”,给领域层和应用层提供实际干活的工具(比如用EF Core实现仓储接口、用Redis存数据),所以它得依赖领域层和应用层,知道人家需要什么工具;最外面的表现层(就是API控制器那些),它是“传话筒”,接收用户请求后,喊应用层来处理,所以它得依赖应用层和基础设施层。你看,整个依赖关系是从外往里指的,最核心的领域层被层层保护着,就像老北京四合院,中间的正房(领域层)最尊贵,其他厢房(应用层、基础设施层、表现层)都围着它转,这样业务逻辑才不会被技术代码带偏。

    这种依赖设计的好处,我之前在电商项目里可是实打实尝到过甜头。那会儿我们要把数据库从SQL Server换成PostgreSQL,按以前的老写法,怕是得把Service层、Repository层翻个底朝天改代码。但用了DDD的四层架构后,我们只动了基础设施层——把EF Core的数据库上下文配置改了改,仓储实现里的连接字符串换了下,领域层和应用层的代码一行没动,三天就搞定了切换。你想啊,业务逻辑(领域层)和技术实现(基础设施层)彻底分开了,以后不管是换ORM框架(比如从EF Core换成Dapper),还是加缓存(Redis换成Memcached),都不用碰核心的业务代码,这不就省了大把返工的功夫?所以说,把依赖关系理清楚,可比多写几行代码重要多了,这才是DDD能让系统“抗造”的关键。


    什么是领域驱动设计(DDD)?和传统开发相比有什么优势?

    领域驱动设计(DDD)是一种以业务领域为核心,通过建模解决复杂业务逻辑的开发方法论。它强调“先理解业务,再设计系统”,将业务规则封装在领域层,避免技术实现与业务逻辑耦合。和传统开发(如三层架构直接堆砌业务)相比,DDD的优势在于:可维护性更高(业务规则集中管理,改需求时无需到处找代码)、扩展性更强(分层架构解耦,技术框架更换不影响业务代码)、团队协作更顺畅(领域模型成为业务与技术的“共同语言”,减少沟通成本)。尤其适合业务逻辑复杂的系统(如电商订单、金融交易)。

    在.NET Core中实现DDD时,四层架构(领域层、应用层、基础设施层、表现层)的依赖关系是怎样的?

    四层架构的依赖关系遵循“依赖倒置原则”,即上层依赖下层的抽象而非具体实现:领域层(核心业务)不依赖任何其他层;应用层(业务协调)仅依赖领域层;基础设施层(技术实现)依赖领域层和应用层(提供仓储、数据库等技术实现);表现层(用户交互)依赖应用层和基础设施层(调用应用服务处理请求)。这种依赖关系确保业务逻辑独立于技术框架,比如更换数据库时只需修改基础设施层,领域层代码无需变动。

    如何在.NET Core中实现领域事件(Domain Event)?

    领域事件用于解耦领域层内部或跨领域的业务流程(如订单支付后触发库存扣减)。在.NET Core中实现步骤通常是:

  • 定义领域事件类(继承IDomainEvent接口,包含事件数据);
  • 在领域实体中添加事件发布逻辑(如Order.Pay()方法中调用AddDomainEvent());3. 使用中介库(如MediatR)实现事件订阅与处理(在基础设施层定义事件处理器,处理具体业务动作如扣减库存);4. 在工作单元(UnitOfWork)中统一发布事件(确保事务提交后触发事件,避免数据不一致)。
  • 聚合根(Aggregate Root)的划分有什么原则?如何避免设计过大的聚合根?

    聚合根划分需遵循两个核心原则:

  • “高内聚,低耦合”:聚合根内部对象(实体、值对象)必须紧密关联,共同完成一个业务功能(如订单+订单项+地址);
  • “事务边界”:聚合根内的操作需在同一事务中完成,确保数据一致性。避免过大聚合根的方法:按“单一职责”拆分(一个聚合根只负责一个核心业务场景)、通过领域事件跨聚合根协作(如订单支付后通过事件通知库存聚合根,而非直接引用)、控制聚合根包含的对象数量(通常不超过5-8个对象)。
  • 什么样的项目适合用DDD+.NET Core实现?小项目用DDD会不会太复杂?

    DDD更适合业务逻辑复杂、需求频繁变更的中大型项目(如电商、金融、ERP系统),这类项目需要长期维护,DDD的分层架构和领域模型能显著降低后期维护成本。小项目(如简单的CRUD工具、内部管理系统)若业务逻辑简单,用传统三层架构可能更高效,避免过度设计。但即便是小项目,也可借鉴DDD的“领域模型优先”思想(先梳理业务规则再写代码),避免后期业务膨胀后重构困难。.NET Core的轻量化特性(类库项目、依赖注入)能灵活适配不同规模项目,无需担心框架本身增加复杂度。

    0
    显示验证码
    没有账号?注册  忘记密码?