Projektowanie modeli domenowych - API .Net Core

0

Cześć, chciałbym dowiedzieć się jak podchodzicie do projektowania modeli domenowych w swoich projektach :)

Przykład:
Chciałbym stworzyć małe API w .Net Core, dosłownie kilka modeli, jakieś podstawowe relacje między nimi (np. wiele do wielu). Planuję wykorzystać EF, zaimplementować CQS. Całość w architekturze cebuli.

Jak zaprojektować modele ? Lepiej zrobić je anemiczne (co wg. mnie ułatwi pracę z EF), czy raczej bogate (z metodami i konstruktorami) ? Niestety mam tutaj małe doświadczenie, w pracy miałem styczność tylko i wyłącznie z anemicznymi.

1
mgrzegor napisał(a):

Cześć, chciałbym dowiedzieć się jak podchodzicie do projektowania modeli domenowych w swoich projektach :)

Zazwyczaj ich nie ma, więc nie trzeba projektować.

Jak zaprojektować modele ? Lepiej zrobić je anemiczne (co wg. mnie ułatwi pracę z EF), czy raczej bogate (z metodami i konstruktorami) ?

A czemu nie oba? Inaczej się używa DAO, inaczej się pisze logikę biznesową.

0
mgrzegor napisał(a):

Jak zaprojektować modele ? Lepiej zrobić je anemiczne (co wg. mnie ułatwi pracę z EF), czy raczej bogate (z metodami i konstruktorami) ? Niestety mam tutaj małe doświadczenie, w pracy miałem styczność tylko i wyłącznie z anemicznymi.

Jeśli jest to domena biznesowa, która później jest przekształcana i mapowana już na encje bazodanowe w konfiguracji EF no to z racji, że jest to domena biznesowa to siłą rzeczy powinna być ona bogata w logikę (w zależności od stopnia jej skomplikowania), a nie głupia i anemiczna. Jedyna "trudność" z EF wtedy to "zmapowanie" takiej domeny na kolumny bazodanowe.

1

Jeśli robisz coś małego to czemu nie zrobić tak jak piszesz. Ja bym nawet nie kusił się tutaj o nazywanie tego modelem domenowym. Po prostu niech Twoja aplikacja ma encje, zastosuj wzorzec mediator do ładnego oddzielenia kontrolerów od logiki operującej na danych i umieść logikę "biznesową" w handlerach.

Prosta aplikacja to proste rozwiązanie. Proste ;)

1

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 :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1