Skip to main content
 首页 » 编程设计

c#之避免将私有(private)集合属性暴露给 Entity Framework 。 DDD原则

2024年01月29日28jillzhang

我尝试在 C# 集合上遵守 DDD 原则 see more here

而且我注意到初始种子 HasData 的模型构建器方法依赖于 ICollection 的添加方法。当从数据库更新/迁移过程中调用该方法时,有什么方法可以规避或欺骗该方法?

到目前为止,我为欺骗它所做的一切都遵循这条路径。

1) 在名为 ReadOnlyKollection

的 ICollection 周围创建一个包装器

2) 在模型上有一个私有(private)的 ICollection,以避免将集合暴露给外界。

3) 公开包装器,使过时的 Add 和一些其他方法在使用时会抛出 NotImplementedException。

然而,尽管有过时的警告,仍然可以使用 Add 方法,因为它仍然是公共(public)的,并且需要更新/迁移数据库过程中使用的种子 HasData 方法。

我正在考虑至少限制从包装类的 Add 方法内部调用方法。

当 HasData 运行并且只允许此方法处理并为任何其他方法抛出异常时,我可能很高兴知道调用成员。

请注意,CallerMethodName 编译类型功能不能使用,因为这会破坏 ICollectoion 接口(interface)协定。

有什么想法可以避免遵循 DDD 原则将私有(private)集合属性暴露给 Entity Framework ? (并且仍然有 HasData 方法的增强来更新/迁移数据库过程)。请参阅下面的一些代码..

public interface IReadOnlyKollection<T> : ICollection<T> 
{ 
} 
 
public class ReadOnlyKollection<T> : IReadOnlyKollection<T> 
{ 
    private readonly ICollection<T> _collection; 
 
    public ReadOnlyKollection(ICollection<T> collection) 
    { 
        _collection = collection; 
    } 
 
    public int Count => _collection.Count; 
    public bool IsReadOnly => _collection.IsReadOnly; 
 
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 
    public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator(); 
 
    public bool Contains(T item) => _collection.Contains(item); 
    public void CopyTo(T[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); 
 
    [Obsolete] 
    public void Add(T item) => _collection.Add(item); // CallerMethodName trick to be applied here or ?? 
 
    [Obsolete]  
    public void Clear() => throw new NotImplementedException(); 
 
    [Obsolete]  
    public bool Remove(T item) => throw new NotImplementedException(); 
} 
 
public class StateProvince 
{ 
    public StateProvince() //EF Constructor 
    { 
    } 
 
    public StateProvince(string id, string name) 
    : this(name) 
    { 
        Id = id; 
    } 
 
    public string Id { get; protected set; } 
    public string Name { get; protected set; } 
 
    public string CountryRegionId { get; protected set; } 
    public virtual CountryRegion CountryRegion { get; protected set; } 
} 
 
public class CountryRegion 
{ 
    public CountryRegion() //EF Constructor 
    { 
    } 
 
    public CountryRegion(string id, string name) 
    : this(name) 
    { 
        Id = id; 
    } 
 
    public string Id { get; protected set; } 
    public string Name { get; protected set; } 
 
    private readonly ICollection<StateProvince> _stateProvinces = new List<StateProvince>(); // Private collection for DDD usage 
    public IReadOnlyKollection<StateProvince> StateProvinces => new ReadOnlyKollection<StateProvince>(_stateProvinces); // Public like read only collection public immutable exposure 
} 
 
 
EntityTypeBuilder<StateProvince> // Code reduced for brevity 
 
builder.HasIndex(e => e.CountryRegionId); 
builder.Property(e => e.Id).IsUnicode(false).HasMaxLength(3).ValueGeneratedNever(); 
builder.Property(e => e.CountryRegionId).IsRequired().IsUnicode(false).HasMaxLength(3); 
builder.Property(e => e.Name).IsRequired().HasMaxLength(50); 
 
 
EntityTypeBuilder<CountryRegion> builder // Code reduced for brevity 
 
builder.Property(e => e.Id).IsUnicode(false).HasMaxLength(3).ValueGeneratedNever(); 
builder.Property(e => e.Name).IsRequired().HasMaxLength(50); 
 
builder.HasMany(e => e.StateProvinces) 
    .WithOne(e => e.CountryRegion) 
    .HasForeignKey(e => e.CountryRegionId) 
    .IsRequired() 
    .OnDelete(DeleteBehavior.Restrict); 
 
builder.HasData(GetData())   
 
private static object[] GetData() 
{    
    return new object[] 
    { 
        new { Id = "AF", Name = "Afghanistan", IsDeleted = false, LastModified = DateTimeOffset.UtcNow  }, 
        new { Id = "AL", Name = "Albania", IsDeleted = false, LastModified = DateTimeOffset.UtcNow  }, 
        new { Id = "DZ", Name = "Algeria", IsDeleted = false, LastModified = DateTimeOffset.UtcNow  }, 
        new { Id = "AS", Name = "American Samoa", IsDeleted = false, LastModified = DateTimeOffset.UtcNow  }, 

请您参考如下方法:

链接的帖子是针对 EF6 的,而 HasData方法表示 EF Core。在 EF Core 中,事情要简单得多,在这方面不需要任何技巧。

  • EF Core 不需要 ICollection<T>用于集合导航属性。任何公共(public)属性(property)返回IEnumerable<T>或派生接口(interface)/类按照惯例作为集合导航属性被发现。因此,您可以安全地将您的收藏公开为 IEnumerable<T> , IReadOnlyCollection<T> , IReadOnlyList<T>等等

  • EF Core 不需要属性 setter ,因为它可以配置为使用 backing field直接。

此外,不需要特殊的“EF 构造函数”,因为 EF Core 支持 constructors with parameters .

话虽如此,您不需要自定义集合接口(interface)/类。示例模型可能是这样的:

public class CountryRegion 
{ 
    public CountryRegion(string name) => Name = name;     
    public CountryRegion(string id, string name) : this(name) => Id = id; 
 
    public string Id { get; protected set; } 
    public string Name { get; protected set; } 
 
    private readonly List<StateProvince> _stateProvinces = new List<StateProvince>(); // Private collection for DDD usage 
    public IReadOnlyCollection<StateProvince> StateProvinces => _stateProvinces.AsReadOnly(); // Public like read only collection public immutable exposure 
} 
 
public class StateProvince 
{ 
    public StateProvince(string name) => Name = name; 
    public StateProvince(string id, string name) : this(name) => Id = id; 
 
    public string Id { get; protected set; } 
    public string Name { get; protected set; } 
 
    public string CountryRegionId { get; protected set; } 
    public virtual CountryRegion CountryRegion { get; protected set; } 
} 

并添加以下内容(最简单 - 对于所有实体的所有属性)

modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);     

或 CountryRegion 的所有属性

builder.UsePropertyAccessMode(PropertyAccessMode.Field); 

或者只是为了那个导航属性

builder.HasMany(e => e.StateProvinces) 
    .WithOne(e => e.CountryRegion) 
    .HasForeignKey(e => e.CountryRegionId) 
    .IsRequired() 
    .OnDelete(DeleteBehavior.Restrict) 
    .Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field); 

仅此而已。您将能够使用所有 EF Core 功能,例如 Include/ThenInclude ,在 LINQ to Entities 查询中“导航”等(包括 HasData )。支持字段允许 EF Core 在需要时添加/删除元素,甚至替换整个集合(如果该字段不是只读的)。