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
相关产品推荐
相关产品推荐

