Cerco di aderire ai principi DDD sulle collezioni C # vedi di più qui
E noto che il metodo di creazione del modello per HasData seed iniziale si basa sul metodo Add di ICollection . C'è un modo per aggirare o ingannare quel metodo quando viene chiamato dal processo di aggiornamento / migrazione del database?
Tutto ciò che avevo fatto finora per ingannare segue questo percorso.
1) Crea un wrapper attorno a ICollection chiamato ReadOnlyKollection
2) Avere una collezione privata sul modello, per evitare di esporre al mondo esterno la collezione.
3) Esporre il wrapper rendendo obsoleto Add e alcuni altri metodi che genereranno NotImplementedException se utilizzato.
Tuttavia, è ancora possibile utilizzare il metodo Add nonostante l'avvertimento obsoleto poiché è ancora pubblico e necessario per il metodo HasData seed utilizzato nel processo di aggiornamento / migrazione del database.
Sto pensando almeno di limitare i metodi di chiamata all'interno del metodo Add della classe wrapper.
Potrei essere buono a conoscere il membro chiamante quando HasData verrà eseguito e consentire solo a questo metodo di elaborare e generare un'eccezione per qualsiasi altro.
Si noti che la funzione di compilazione CallerMethodName non può essere utilizzata in quanto interromperà il contratto di interfaccia ICollectoion.
Qualche idea per evitare di esporre le proprietà di collezioni private a Entity Framework seguendo i principi DDD? (e ha ancora il miglioramento del metodo HasData per aggiornare / migrare il processo del database). vedi un po 'di codice qui sotto ..
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 },
Il post collegato è per EF6, mentre il metodo HasData
indica EF Core. E in EF Core le cose sono molto più semplici e non hanno bisogno di trucchi in tal senso.
EF Core non richiede ICollection<T>
per la proprietà di navigazione della raccolta. Qualsiasi proprietà pubblica che restituisce IEnumerable<T>
o un'interfaccia / classe derivata viene scoperta per convenzione come proprietà di navigazione della raccolta. Quindi puoi esporre in sicurezza le tue raccolte come IEnumerable<T>
, IReadOnlyCollection<T>
, IReadOnlyList<T>
ecc.
EF Core non richiede il setter proprietà perché può essere configurato per utilizzare direttamente il campo di supporto .
Inoltre, non è necessario uno speciale "EF Constructor" perché EF Core supporta costruttori con parametri .
Detto questo, non è necessaria un'interfaccia / classe di raccolta personalizzata. Il modello di esempio potrebbe essere così:
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; }
}
e aggiungi quanto segue (il più semplice - per tutte le proprietà di tutte le entità)
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);
o per tutte le proprietà di CountryRegion
builder.UsePropertyAccessMode(PropertyAccessMode.Field);
o solo per quella proprietà di navigazione
builder.HasMany(e => e.StateProvinces)
.WithOne(e => e.CountryRegion)
.HasForeignKey(e => e.CountryRegionId)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
E questo è tutto. Sarai in grado di utilizzare tutte le funzionalità di EF Core come Include
/ ThenInclude
, "navigando" all'interno delle query LINQ to Entities ecc. ( HasData
). Il supporto archiviato consente a EF Core di aggiungere / rimuovere elementi quando necessario, o addirittura di sostituire l'intera raccolta (nel caso in cui il campo non sia di sola lettura).