Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Comparison

"No tool is right for every situation. The question is not 'is Entity.Dsl good?' but 'is it good for this project?'"


The Landscape

When building an EF Core data layer in .NET, developers choose from several approaches. Each has trade-offs. This part compares Entity.Dsl against six alternatives, then offers a decision framework.


vs Hand-Written Fluent API

The baseline. Most .NET developers write EF Core configurations by hand using the Fluent API.

The Same Entity, Two Ways

Hand-written (35 lines):

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("Orders");
        builder.HasKey(e => e.Id);
        builder.Property(e => e.Id).ValueGeneratedOnAdd();
        builder.Property(e => e.OrderNumber).IsRequired().HasMaxLength(50);
        builder.Property(e => e.Total).HasPrecision(18, 2);
        builder.Property(e => e.Status).HasConversion<string>();

        builder.HasMany(e => e.Items)
            .WithOne(e => e.Order)
            .HasForeignKey(e => e.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasOne(e => e.Customer)
            .WithMany(e => e.Orders)
            .HasForeignKey(e => e.CustomerId)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

Plus a separate IOrderRepository, OrderRepository, DbContext registration, and DI wiring.

Entity.Dsl (15 lines):

[AggregateRoot("Order")]
[Table("Orders")]
[Timestampable]
[Blameable]
public partial class Order
{
    [PrimaryKey]
    public Guid Id { get; set; }
    [Required]
    [MaxLength(50)]
    public string OrderNumber { get; set; } = "";
    [Precision(18, 2)]
    public decimal Total { get; set; }
    [EnumStorage(AsString = true)]
    public OrderStatus Status { get; set; }
    public Guid CustomerId { get; set; }
    [Aggregation]
    [HasOne(WithMany = "Orders", ForeignKey = "CustomerId")]
    public Customer Customer { get; set; } = null!;
    [Composition]
    [HasMany(WithOne = "Order", ForeignKey = "OrderId")]
    public List<OrderItem> Items { get; set; } = new();
}

Configuration, repository, UoW, DI registration — all generated.

Comparison

Criterion Hand-Written Entity.Dsl
Lines of code per entity ~60 (config + repo + DI) ~15 (attributed POCO)
Consistency across entities Depends on developer discipline Guaranteed by generator
Delete behavior correctness Manual — easy to forget DDD attribute → correct default
Repository pattern Manual per entity Generated with [Injectable]
Customization Full control Full control via Generation Gap
Learning curve EF Core Fluent API Entity.Dsl attributes + EF Core
Debugging Straightforward Generated code is readable but indirect

Verdict: For projects with 5+ entities, Entity.Dsl saves significant time and eliminates consistency drift. For 1-3 entities, hand-written code is simpler.


vs EF Core Conventions

EF Core's built-in conventions handle a lot without explicit configuration: FK inference from {Navigation}Id properties, cascade delete for non-nullable FKs, table names from DbSet property names.

What Conventions Get Right

  • FK naming: CustomerId on Order automatically becomes the FK to Customer
  • Cascade behavior: Non-nullable FK → Cascade, nullable FK → ClientSetNull
  • Table names: DbSet<Order> Orders → table name Orders
  • Key detection: Property named Id or {ClassName}Id is automatically the PK

What Conventions Miss

  • No DDD semantics: Conventions do not know about Composition vs Aggregation. A non-nullable FK always cascades — even when you want Restrict (e.g., Order → Customer).
  • No repository generation: Conventions configure mappings, not infrastructure.
  • No behaviors: No Timestampable, SoftDeletable, Blameable, Sluggable.
  • No Generation Gap: No way to override a specific convention for a specific entity without writing a full IEntityTypeConfiguration<T>.
  • Convention drift: When conventions surprise you (wrong cascade, unexpected column name), the fix is a Fluent API override. Over time, the project accumulates exceptions to conventions.

Comparison

Criterion EF Core Conventions Entity.Dsl
Setup cost Zero (built-in) NuGet packages + learning
FK inference Yes Yes (plus explicit [HasMany]/[HasOne])
Delete behavior Based on nullability Based on DDD relationship type
Repository/UoW No Generated
Behaviors No Five built-in
Explicitness Implicit (invisible rules) Explicit (attributes = documentation)

Verdict: Conventions are a great starting point. Entity.Dsl is for when you outgrow conventions — when you need explicit DDD semantics, generated infrastructure, and cross-cutting behaviors.


vs Data Annotations

System.ComponentModel.DataAnnotations provides attributes like [Required], [MaxLength], [Key], [Table], [Column]. They look similar to Entity.Dsl's attributes.

What Data Annotations Provide

[Table("Orders")]
public class Order
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string OrderNumber { get; set; } = "";

    [Column(TypeName = "decimal(18,2)")]
    public decimal Total { get; set; }
}

What They Cannot Do

  • No relationship configuration (no [HasMany], [HasOne], [Composition])
  • No delete behavior control
  • No repository generation
  • No behaviors (Timestampable, SoftDeletable, etc.)
  • No Generation Gap — cannot override a specific property's configuration
  • Limited to simple scalar mapping

Data Annotations are a subset of what Fluent API can do. Entity.Dsl attributes are a superset — they express DDD semantics, relationship types, behaviors, and generate infrastructure.

Comparison

Criterion Data Annotations Entity.Dsl
Relationship config No Yes
Delete behavior No Yes (DDD-aware)
Behaviors No Five built-in
Generated infrastructure No Yes
Standard .NET Yes (System namespace) No (custom package)
Attribute density Low (few per entity) Higher (more attributes)

Verdict: Data Annotations are fine for simple CRUD with no DDD. Entity.Dsl is for domain-rich applications where relationships, lifecycle, and behaviors matter.


vs EF Core Compiled Models

EF Core Compiled Models (introduced in EF Core 6) pre-compile the model at build time to reduce startup time. They are a performance optimization, not a modeling approach.

Entity.Dsl and Compiled Models are complementary, not competitive. Entity.Dsl generates the configuration. Compiled Models pre-compile that configuration for faster startup.

<!-- Use both: Entity.Dsl generates the config, Compiled Models pre-compile it -->
<PackageReference Include="FrenchExDev.Net.Entity.Dsl.SourceGenerator"
                  OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

<!-- EF Core build-time compilation -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" />

vs NHibernate / FluentNHibernate

NHibernate was the original .NET ORM, predating EF Core by over a decade. It solved the same problem Entity.Dsl addresses — but with different tools.

Criterion NHibernate XML FluentNHibernate Entity.Dsl
Configuration format XML mappings C# fluent code Attributes + source generation
Runtime vs compile-time Runtime (XML parsing) Runtime (reflection) Compile-time (source generation)
IDE support XML schema validation IntelliSense for fluent API IntelliSense + generated code navigation
Repository generation No No Yes
Convention support Yes (auto-mapping) Yes (conventions) Yes (attribute-driven)
Performance overhead XML parsing + reflection Reflection Zero runtime overhead
Maturity 20+ years 15+ years Newer
Ecosystem Established, fewer new features Stable Growing

NHibernate's approach was revolutionary for its time. Entity.Dsl applies the same idea — declarative mapping — but leverages modern tooling (Source Generators, Roslyn) to eliminate runtime costs and generate more infrastructure.

Verdict: If you are already on NHibernate, migration is a significant effort. For new projects on EF Core, Entity.Dsl is a more modern approach to the same problem NHibernate solved.


vs Dapper / Micro-ORMs

Dapper and other micro-ORMs take a fundamentally different approach: SQL-first, with thin mapping.

// Dapper: you write the SQL, Dapper maps the result
var orders = await connection.QueryAsync<Order>(
    "SELECT * FROM Orders WHERE CustomerId = @CustomerId ORDER BY OrderDate DESC",
    new { CustomerId = customerId });

Different Philosophies

Dapper (SQL-first) Entity.Dsl (Model-first)
Query language Raw SQL LINQ (translated to SQL)
Change tracking No Yes
Migrations Manual (or tool-assisted) EF Core Migrations
Performance Maximum (raw SQL) Good (EF Core overhead)
Productivity Lower (write SQL) Higher (LINQ + generated repos)
Schema changes Manual SQL updates Automatic migration generation
Complex joins SQL expertise needed LINQ Include/ThenInclude
Bulk operations Fast (raw SQL) Slower (tracked entities)

When Dapper Is Better

  • Read-heavy: Reporting, dashboards, analytics — raw SQL with Dapper is faster and more readable than complex LINQ
  • Existing schema: Database-first projects where the schema is owned by DBAs
  • Performance-critical: Sub-millisecond queries where EF Core's overhead matters
  • Simple CRUD: No DDD, no behaviors, no aggregate boundaries — just tables

When Entity.Dsl Is Better

  • DDD-rich domains: Aggregates, compositions, value objects, lifecycle ownership
  • Team standardization: Generated infrastructure ensures consistency across developers
  • Rapid development: LINQ + generated repositories + UoW vs writing SQL + mapping
  • Schema evolution: EF Core Migrations from the model, not manual DDL

Verdict: Dapper and Entity.Dsl solve different problems. Many projects use both — Entity.Dsl for the write side (domain model, business logic) and Dapper for the read side (queries, reports, projections).


vs Other Source Generators

Several EF Core source generators exist in the .NET ecosystem:

Generator Focus DDD-Aware? Behaviors? Generation Gap?
Entity.Dsl Full domain infrastructure Yes Yes (5 built-in) Yes
EF Core Scaffolding Database-first reverse engineering No No No
EntityFrameworkCore.Generator Configuration from model conventions No No No
Meziantou.Polyfill Backporting newer EF Core APIs No No No

Entity.Dsl's differentiators:

  • DDD awareness: Composition/Aggregation/Association → delete behavior
  • Behavior system: Timestampable, SoftDeletable, Blameable, Sluggable, Versionable
  • Generation Gap pattern: Three-layer override hierarchy
  • Injectable integration: Automatic DI registration via [Injectable]
  • Association classes: First-class M:N with payload and specialized repositories
  • Emit model architecture: DSL-to-DSL generation capability

Entity.Dsl Shines When

  • Domain-rich: Aggregates, compositions, value objects, lifecycle ownership
  • Team consistency: Multiple developers, need to prevent convention drift
  • Large models: 10+ entities where generated infrastructure pays for itself
  • DDD-first: The domain model drives the persistence, not the other way around
  • Behaviors needed: Timestamps, soft-delete, audit trails are common across entities

Entity.Dsl Is Overkill When

  • Small projects: 1-5 entities, simple CRUD, no DDD
  • Database-first: Schema is owned by DBAs, you adapt to it
  • Dapper-first: SQL is the primary query language
  • Prototype: Speed of iteration matters more than architecture
  • Team unfamiliarity: If no one on the team knows Source Generators, the learning curve adds risk

Learning Curve

Entity.Dsl introduces new concepts: DDD attributes, the Generation Gap pattern, behavior composition, emit models. A developer familiar with EF Core needs 1-2 days to be productive, and a week to understand the generated code deeply.

Debugging Complexity

Generated code is readable but adds indirection. When a query does not behave as expected, the developer must trace through:

  1. The attribute on the entity → 2. The generated configuration → 3. The EF Core translation → 4. The SQL output.

This is one more step than hand-written code (where step 2 is the developer's own configuration). The trade-off: you trace less often because the generated code is correct by construction.

Build-Time Overhead

Source Generators add time to the build. Entity.Dsl's incremental generator minimizes this, but the first build (cold cache) takes longer. On a large project (50+ entities), expect 1-3 seconds of additional build time.

Dependency Risk

Entity.Dsl is a custom framework. If the project is abandoned or the team moves on, the generated code still works — it is standard EF Core. But the attributes become dead decorators, and new entities must be configured manually. The migration path is: remove the generator, keep the generated .g.cs files, edit them as regular code.

The Exit Strategy

This deserves emphasis because it is often overlooked when evaluating code generation tools. Entity.Dsl's exit strategy is clean:

  1. Enable EmitCompilerGeneratedFiles in your .csproj to write generated files to disk
  2. Copy the generated .g.cs files from obj/GeneratedFiles/ into your source tree
  3. Remove the FrenchExDev.Net.Entity.Dsl.SourceGenerator NuGet reference
  4. Remove OutputItemType="Analyzer" from the package reference
  5. Keep the Entity.Dsl.Abstractions and Ddd.Attributes packages (they are runtime dependencies)
  6. Build — everything compiles. The generated files are now regular source files.

From this point, the project uses standard EF Core with standard files. The attributes remain as documentation but have no effect. New entities are configured manually. Existing entities continue to work.

This is possible because Entity.Dsl generates standard EF Core code, not a custom runtime. There is no Entity.Dsl runtime layer, no proxy classes, no reflection hooks. The generator produces the same code a developer would write by hand — just automatically.


Pattern 1: Greenfield with Entity.Dsl from Day One

Start a new project with Entity.Dsl. Every entity uses attributes. The team learns the pattern together.

Pros: Maximum consistency, maximum ROI, team builds muscle memory early.

Cons: Learning curve front-loaded. First sprint may be slower.

Pattern 2: Incremental Adoption in Brownfield

Add Entity.Dsl to an existing project. New entities use attributes. Existing entities keep their hand-written configurations.

Pros: No migration risk. Team learns incrementally. Existing code untouched.

Cons: Two patterns coexist. Cognitive load increases temporarily. Generated and hand-written configurations must not conflict.

Pattern 3: Entity.Dsl for Write Side, Dapper for Read Side

Use Entity.Dsl for the domain model (commands, mutations, business logic) and Dapper for the query side (reports, dashboards, search).

Pros: Best of both worlds. Domain model gets generated infrastructure. Queries get raw SQL performance.

Cons: Two data access patterns. Team needs both skill sets.

Pattern 4: Entity.Dsl as Reference Implementation

Use Entity.Dsl to generate the initial infrastructure. Then eject (exit strategy above) and maintain the generated code by hand.

Pros: Fast bootstrapping. Generated code serves as a high-quality starting point.

Cons: Loses ongoing generation benefits. One-time ROI.


A Word on Code Generation Philosophy

Entity.Dsl belongs to a broader movement in .NET: attribute-driven code generation. The pattern appears across the ecosystem:

  • [JsonSerializable] (System.Text.Json) — generates serializers at compile time
  • [LoggerMessage] (Microsoft.Extensions.Logging) — generates high-performance log methods
  • [GeneratedRegex] (System.Text.RegularExpressions) — compiles regex at build time
  • [Injectable] (FrenchExDev) — generates DI registration from attributes
  • Entity.Dsl — generates EF Core infrastructure from domain attributes

The pattern is consistent: declare intent with an attribute, let the compiler generate the implementation. Each generation shifts work from runtime to compile time, reduces boilerplate, and makes the attribute itself serve as documentation.

Entity.Dsl extends this pattern to the domain layer — the part of the application where most of the complexity lives and where consistency matters most.


Feature-by-Feature Comparison Table

A detailed breakdown of what each approach supports out of the box:

Feature Hand-Written Conventions Data Annotations NHibernate Dapper Entity.Dsl
Table mapping Manual Auto [Table] XML/Fluent N/A [Table]
PK configuration Manual Convention [Key] XML/Fluent N/A [PrimaryKey]
Composite keys Manual No No XML/Fluent N/A [PrimaryKey(Order)]
Column mapping Manual Convention [Column] XML/Fluent N/A [Column]
Required/MaxLength Manual No [Required]/[MaxLength] XML/Fluent N/A [Required]/[MaxLength]
Relationships Manual Fluent Auto FK No XML/Fluent N/A [HasMany]/[HasOne]
Delete behavior Manual Nullability No XML/Fluent N/A DDD-aware auto
Composition/Aggregation Manual No No No N/A [Composition]/[Aggregation]
Owned types Manual No No Component N/A [Owned]/[OwnedEntity]
Complex types Manual No No No N/A [ComplexType]/[ValueObject]
Inheritance (TPH/TPT/TPC) Manual No No XML/Fluent N/A [Inheritance]
Many-to-many Manual Auto (EF5+) No XML/Fluent N/A [ManyToMany]/[AssociationClass]
Self-reference Manual No No XML/Fluent N/A [SelfReference]
Timestamps Manual No No Interceptor N/A [Timestampable]
Soft delete Manual No No Filter N/A [SoftDeletable]
Audit trail Manual No No Interceptor N/A [Blameable]
URL slugs Manual No No No N/A [Sluggable]
Concurrency Manual No [Timestamp] Versioning N/A [Versionable]
Repository generation No No No No No Yes
UnitOfWork generation No No No No No Yes
DI registration Manual No No No No [Injectable] auto
Generation Gap No No No No N/A Yes (3-layer)
Compile-time validation No No No No N/A Diagnostics (EDSL0xxx)
Enum storage Manual Convention No XML N/A [EnumStorage]
Computed columns Manual No No Formula N/A [ComputedColumn]
Default values Manual No No XML N/A [DefaultValue]
Database comments Manual No No No N/A [Comment]
JSON columns Manual No No No N/A [OwnedEntity(JsonColumn)]
Specification pattern Manual No No Criteria Manual Built-in ISpecification<T>
Association repository Manual No No No No IAssociationRepository auto
Custom repo base Manual No No Manual N/A [DbContext(RepositoryBase)]

Summary Matrix

Approach Best For Generated Code DDD-Aware Behaviors Learning Curve
Hand-written Fluent API Small projects, full control No No No Low
EF Core Conventions Convention-over-configuration shops No No No Lowest
Data Annotations Simple CRUD, minimal mapping No No No Low
NHibernate Legacy projects, XML-era No Partial Partial Medium
Dapper Read-heavy, SQL-first No No No Low
Entity.Dsl DDD-rich domains, team consistency Yes (13x amplification) Yes Yes (5 built-in) Medium

"Does Entity.Dsl support database-first workflows?"

No. Entity.Dsl is model-first by design. You define entities with attributes, and the generator produces configurations. If your schema is owned by DBAs, use EF Core scaffolding or Dapper. Entity.Dsl is for projects where the domain model drives the database schema.

"Can I use Entity.Dsl with non-SQL databases?"

Entity.Dsl generates standard EF Core configuration. If EF Core supports your database (Cosmos DB, PostgreSQL, MySQL, SQLite), Entity.Dsl works. However, database-specific features (JSON columns, PostGIS geography, Cosmos DB partitioning) require raw Fluent API in the partial Configuration class.

"How does Entity.Dsl handle migrations?"

Entity.Dsl does not generate migrations. It generates EF Core configurations. You use dotnet ef migrations add as usual. The migration tooling reads the generated configurations and produces migration files. This is transparent — the migration tool does not know or care that the configurations were generated.

"Can I use Entity.Dsl in a project that already has hand-written configurations?"

Yes. Entity.Dsl generates configurations for attributed entities only. Hand-written configurations for non-attributed entities continue to work. The two approaches coexist — the generated DbContext's RegisterConfigurations method only registers configurations for attributed entities. You call modelBuilder.ApplyConfiguration(new MyHandWrittenConfig()) in the PostModelCreating hook for the rest.

"What happens if I remove an attribute?"

The generator stops producing files for that entity. The next build will fail with missing type errors (the repository interface, the configuration, etc.). Remove the references to the generated types, and the project compiles with one less generated entity. Or add the hand-written equivalents.

"Is there runtime overhead?"

Zero. Entity.Dsl is purely compile-time. The generated code is standard C# that compiles to the same IL as hand-written code. No reflection, no proxy classes, no runtime scanning. The attributes themselves are compiled into the assembly metadata but never read at runtime.


Closing Thought

Entity.Dsl is not a replacement for understanding EF Core. It is a multiplier for developers who already understand it. The generated code is standard EF Core — readable, debuggable, and overridable. The attributes are documentation that the compiler enforces. The behaviors are cross-cutting concerns that compose without conflict.

The pitch from Part I holds: you describe what your domain model is. The compiler produces how EF Core configures it. And if the compiler gets it wrong, the Generation Gap pattern lets you fix it without losing the automation.

The marketplace domain we built — 14 entities, 7 aggregates, 5 behaviors, 1 inheritance hierarchy — is real-world complexity. Entity.Dsl handled it with 210 lines of input and 2,800 lines of generated output. Whether that trade-off is worth it depends on your project, your team, and your domain.

For domains rich enough to need DDD patterns, the answer is usually yes.