Zasadniczo modele anemiczne są antywzorcem, chociaż nie za bardzo wiem czemu. Pewnie po prostu jak zwykle - wszystko zależy od projektu.
@XardasLord był tak uprzejmy i stwierdził, że Jedyna "trudność" z EF wtedy to "zmapowanie" takiej domeny na kolumny bazodanowe
. Jest to po części prawda, jeśli Twoje modele są w jakiś sposób rozrośnięte, występuje dziedziczenie itd. Ale na własne potrzeby zmontowałem kiedyś taki mechanizm mapowania podobny do tego z nHibernate. Moim założeniem było:
- tylko te pola, które chcę i jak chcę
- żadnych atrybutów.
A niech tam. Podzielę się. Kod nie jest doskonały, cały czas jest żywy. Do prostych zastosowań w zupełności wystarczy. Najpierw jest klasa SchemaInfo
, która tak naprawdę trzyma informacje o tym na jakiej bazie działamy:
class SchemaInfo<T> where T : class, IDbItem
{
public EntityTypeBuilder<T> Builder { get; set; }
public DbType DatabaseType { get; set; }
}
IDbItem to jakiś interfejs, który implementują wszystkie encje, które będą w bazie danych. W moim przypadku tam jest tylko jedno pole: Guid Id;
Dalej mamy bazową klasę do mapowania:
abstract class BaseMap<T>: IEntityTypeConfiguration<T> where T: class, IDbItem
{
EntityTypeBuilder<T> theBuilder;
List<string> mappedPropertyNames = new List<string>();
protected EntityTypeBuilder<T> ToTable(string tableName)
{
return theBuilder.ToTable(tableName);
}
protected PropertyBuilder<TProperty> Map<TProperty>(Expression<Func<T, TProperty>> x)
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.Property(x);
}
protected ReferenceNavigationBuilder<T, TRelatedEntity> HasOne<TRelatedEntity>(Expression<Func<T, TRelatedEntity>> x)
where TRelatedEntity: class
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.HasOne(x);
}
protected CollectionNavigationBuilder<T, TRelatedEntity> HasMany<TRelatedEntity>(Expression<Func<T, IEnumerable<TRelatedEntity>>> x)
where TRelatedEntity: class
{
mappedPropertyNames.Add(GetPropertyName(x));
return theBuilder.HasMany(x);
}
protected PropertyBuilder<TColumnType> Map<TColumnType>(string propName)
{
mappedPropertyNames.Add(propName);
return theBuilder.Property<TColumnType>(propName);
}
protected void DontIgnore<TProperty>(Expression<Func<T, TProperty>> x)
{
mappedPropertyNames.Add(GetPropertyName(x));
}
protected void AddMappedColName(string colName)
{
mappedPropertyNames.Add(colName);
}
protected abstract void CreateModel(SchemaInfo<T> schemaInfo);
protected virtual void MapId(SchemaInfo<T> schemaInfo)
{
Map(x => x.Id).ValueGeneratedOnAdd();
theBuilder.HasKey(x => x.Id);
}
protected virtual bool CanIgnoreUnmappedProperties()
{
return true;
}
public void Configure(EntityTypeBuilder<T> builder)
{
theBuilder = builder;
SchemaInfo<T> si = new SchemaInfo<T>();
si.Builder = builder;
si.DatabaseType = DbInitializer.DatabaseType;
MapId(si);
CreateModel(si);
if (mappedPropertyNames.Count > 0 && CanIgnoreUnmappedProperties())
IgnoreUnmappedProperties(builder);
}
void IgnoreUnmappedProperties(EntityTypeBuilder<T> builder)
{
//Debugger.Launch();
PropertyInfo[] propsInModel = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var prop in propsInModel)
{
if (!mappedPropertyNames.Contains(prop.Name))
{
builder.Ignore(prop.Name);
}
}
}
string GetPropertyName<TProperty>(Expression<Func<T, TProperty>> memberExpression)
{
MemberExpression member = null;
switch (memberExpression.Body)
{
case UnaryExpression ue when ue.Operand is MemberExpression:
member = ue.Operand as MemberExpression;
break;
case MemberExpression me:
member = me;
break;
default:
throw new InvalidOperationException("You should pass property to the method, for example: x => x.MyProperty");
}
var pInfo = member.Member as PropertyInfo;
if (pInfo == null)
throw new InvalidOperationException("You should pass property to the method, for example: x => x.MyProperty");
return pInfo.Name;
}
}
I potem mamy konkretne klasy, które mapują konkretne encje (podobnie jak w nHibernate), np:
class DocumentMap : BaseMap<Document>
{
protected override void CreateModel(SchemaInfo<Document> si)
{
ToTable("documents");
Map(x => x.Name).IsRequired();
Map<Guid>("Owner_id").IsRequired();
//this must be saved. See SaveDocumentCommand and read comment near call to SaveDocumentHeader
//It's needed to be able to count all user documents
Map(x => x.StoragePlace).HasConversion<int>();
HasOne(x => x.Owner)
.WithMany()
.HasForeignKey("Owner_id")
.HasConstraintName("FK_Users_Documents")
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
HasMany(x => x.Periods)
.WithOne(y => y.ParentDocument);
HasMany(x => x.Loans)
.WithOne(y => y.ParentDocument);
}
}
Kod jest raczej prosty. Zasadniczo, mapujemy konkretne propertisy metodą Map.
Jest też specjalna metoda MapId w klasie bazowej, którą można przeciążać. Np w klasie mapującej jeśli zrobisz:
protected override void MapId(SchemaInfo<BlacklistedToken> schemaInfo)
{
if (schemaInfo.DatabaseType != Dal.DbType.Mssql)
return;
else
base.MapId(schemaInfo);
}
wtedy nie będzie zmapowane id. Id jest domyślnie mapowane w klasie BaseMap - która używa interfejsu IDbItem.
Teraz tylko kwestia odpowiedniej konfiguracji DbContext:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
//tutaj mapuję modele z Identity
builder.Entity<SystemUser>().ToTable("users");
builder.Entity<UserRole>().ToTable("roles");
builder.Entity<IdentityUserRole<Guid>>().ToTable("user_roles");
builder.Entity<IdentityUserClaim<Guid>>().ToTable("user_claims");
builder.Entity<IdentityUserLogin<Guid>>().ToTable("user_logins");
builder.Entity<IdentityRoleClaim<Guid>>().ToTable("role_claims");
builder.Entity<IdentityUserToken<Guid>>().ToTable("user_tokens");
//no i tu dzieje się cała magia. Całe mapowanie teraz idzie przez klasę BaseMap. Ważne, żebyś zamiast GetExecutingAssembly dał asembly, w którym masz te mapowania.
//u mnie to jest ten sam projekt (EfCoreDataAccess)
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
I tyle. Wszelkie uwagi mile widziane :)