避免将私有集合属性暴露给实体框架。 DDD原则

时间:2019-03-14 21:28:34

标签: c# entity-framework entity-framework-core

我尝试在C#集合see more here上坚持DDD原则

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

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

1)围绕名为 ReadOnlyKollection

的ICollection创建一个包装器

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

3)公开包装器,使其过时的Add和其他一些方法(如果使用的话)将引发NotImplementedException。

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

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

我很高兴知道何时运行HasData并仅允许此Method处理并为其他任何异常抛出异常的调用成员。

请注意,由于将破坏ICollectoion接口协定,因此不能使用CallerMethodName编译类型功能。

有什么想法可以避免遵循DDD原则将私有集合属性暴露给实体框架? (并且仍然具有增强的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  },

1 个答案:

答案 0 :(得分:5)

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

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

  • EF Core不需要属性设置器,因为可以将其配置为直接使用backing field

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

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

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内部“导航”到实体查询等(包括HasData)。提交的后备文件允许EF Core在需要时添加/删除元素,甚至替换整个集合(以防字段不是只读的)。