You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD中如何从仓储返回包含导航属性指定字段的领域实体

解决方案

方案一:为Admin添加专用的轻量构造函数

领域实体可针对ORM投影场景添加私有/受保护构造函数,仅初始化所需字段,给必填敏感字段设置占位符(需确保领域逻辑不会误用该占位符)。

步骤1:修改Admin实体

添加私有构造函数,仅初始化Id和Name,PasswordHash设为占位符:

public class Admin
{
    public AdminId Id { get; }
    public AdminName? Name { get; private set; }
    public string PasswordHash { get; private set; }

    // 业务场景使用的构造函数
    public Admin(AdminId id, AdminName name, string passwordHash)
    {
        Id = id;
        Name = name;
        PasswordHash = passwordHash ?? throw new ArgumentNullException(nameof(passwordHash));
    }

    // 供EF Core投影加载使用的私有构造函数
    private Admin(AdminId id, AdminName? name)
    {
        Id = id;
        Name = name;
        PasswordHash = string.Empty; // 占位符,仅用于只读场景
    }

    // 原有Create方法...
}

步骤2:配置EF Core识别该构造函数

在DbContext的OnModelCreating中,通过Fluent API告知EF Core使用该构造函数:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Admin>()
        .HasConstructor<AdminId, AdminName?>();
}

步骤3:编写投影查询

在仓储中查询Article时,投影生成包含轻量Admin的实体:

public async Task<List<Article>> GetArticlesWithWriterInfoAsync()
{
    return await _dbContext.Articles
        .Select(a => new Article(
            a.Id,
            a.Title,
            a.Body,
            a.WriterId,
            new Admin(a.Writer.Id, a.Writer.Name)
        ))
        .ToListAsync();
}

注意:这种轻量Admin仅适用于只读场景(如展示文章作者信息),若业务需验证密码等操作,必须加载完整的Admin实体。


方案二:拆分Admin为公开实体与凭证实体(推荐)

遵循DDD单一职责原则,将Admin的公开信息(Id、Name)与敏感凭证信息(PasswordHash)拆分为两个独立实体,彻底分离关注点。

步骤1:拆分实体

// 公开的Admin实体,用于展示作者信息等场景
public class Admin : Entity<AdminId>
{
    public AdminName? Name { get; private set; }

    private Admin(AdminId id, AdminName? name) : base(id)
    {
        Name = name;
    }

    public static Result<Admin> Create(AdminId id, AdminName? name)
    {
        var errors = new List<ResultError>();
        if (id is null) errors.Add(AdminErrors.AdminIdIsNull);
        
        return errors.Any() 
            ? Result.Failure<Admin>().WithErrors(errors)
            : Result.Success(new Admin(id!, name));
    }
}

// 敏感的Admin凭证实体,仅用于身份验证场景
public class AdminCredentials : Entity<AdminId>
{
    public string PasswordHash { get; private set; }

    private AdminCredentials(AdminId id, string passwordHash) : base(id)
    {
        PasswordHash = passwordHash ?? throw new ArgumentNullException(nameof(passwordHash));
    }

    public static Result<AdminCredentials> Create(AdminId id, string passwordHash)
    {
        var errors = new List<ResultError>();
        if (id is null) errors.Add(AdminErrors.AdminIdIsNull);
        if (string.IsNullOrEmpty(passwordHash)) errors.Add(AdminErrors.PasswordHashIsNullOrEmpty);
        
        return errors.Any() 
            ? Result.Failure<AdminCredentials>().WithErrors(errors)
            : Result.Success(new AdminCredentials(id!, passwordHash));
    }
}

步骤2:配置EF Core关联关系

在OnModelCreating中配置一对一关联:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Admin与AdminCredentials一对一关联,共享Id
    modelBuilder.Entity<AdminCredentials>()
        .HasOne<Admin>()
        .WithOne()
        .HasForeignKey<AdminCredentials>(ac => ac.Id);
}

步骤3:查询Article时仅加载公开Admin实体

此时查询Article只需关联Admin实体,无需涉及敏感的PasswordHash:

public async Task<List<Article>> GetArticlesWithWriterInfoAsync()
{
    return await _dbContext.Articles
        .Include(a => a.Writer) // 仅加载Admin的Id和Name
        .ToListAsync();
}

当需要验证密码时,单独查询AdminCredentials:

public async Task<AdminCredentials?> GetAdminCredentialsAsync(AdminId adminId)
{
    return await _dbContext.AdminCredentials
        .FirstOrDefaultAsync(ac => ac.Id == adminId);
}

该方案不仅解决了投影问题,还提升了领域模型清晰度,避免敏感数据在不必要场景被加载。


方案三:利用EF Core的Owned Entity(拥有实体)

若不想拆分实体,可将PasswordHash标记为Owned Entity,实现敏感字段的选择性加载:

步骤1:重构Admin实体,包装PasswordHash为Owned类型

public class AdminPassword : ValueObject
{
    public string Hash { get; private set; }

    private AdminPassword(string hash)
    {
        Hash = hash ?? throw new ArgumentNullException(nameof(hash));
    }

    public static Result<AdminPassword> Create(string hash)
    {
        if (string.IsNullOrEmpty(hash))
            return Result.Failure<AdminPassword>().WithError(AdminErrors.PasswordHashIsNullOrEmpty);
        
        return Result.Success(new AdminPassword(hash));
    }

    // 实现ValueObject的Equals和GetHashCode方法...
}

public class Admin
{
    public AdminId Id { get; }
    public AdminName? Name { get; private set; }
    public AdminPassword? Password { get; private set; } // 可选的Owned实体

    public Admin(AdminId id, AdminName name, AdminPassword password)
    {
        Id = id;
        Name = name;
        Password = password;
    }

    // 供投影使用的构造函数
    private Admin(AdminId id, AdminName? name)
    {
        Id = id;
        Name = name;
        Password = null;
    }

    // 原有Create方法...
}

步骤2:配置EF Core Owned Entity

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Admin>()
        .OwnsOne(a => a.Password, p =>
        {
            p.Property(pp => pp.Hash).HasColumnName("PasswordHash");
        });
}

步骤3:查询时排除Password字段

public async Task<List<Article>> GetArticlesWithWriterInfoAsync()
{
    return await _dbContext.Articles
        .Include(a => a.Writer)
        .Select(a => new Article(
            a.Id,
            a.Title,
            a.Body,
            a.WriterId,
            new Admin(a.Writer.Id, a.Writer.Name)
        ))
        .ToListAsync();
}

这种方式无需拆分实体即可实现敏感字段选择性加载,但需注意领域逻辑中对Password为null的处理。


内容的提问来源于stack exchange,提问作者Hossam Fares

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 19:54:50