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

The Full Domain

"14 entities. 180 lines of developer code. 2,800 lines of generated infrastructure. 42+ files. Zero hand-written plumbing."


All Entity Definitions

Here is every entity class in the marketplace domain, assembled in one place. This is the complete developer-written code.

Store Aggregate

[AggregateRoot("Store")]
[Table("Stores")]
[Timestampable]
[Versionable]
public partial class Store
{
    [PrimaryKey]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";

    [Required]
    [MaxLength(500)]
    public string Description { get; set; } = "";

    [MaxLength(2000)]
    public string? WebsiteUrl { get; set; }

    public Address? Address { get; set; }

    [Composition]
    [HasOne(ForeignKey = "StoreId")]
    public StoreSettings? Settings { get; set; }

    public List<Product> Products { get; set; } = new();
}

[Entity("StoreSettings")]
[Table("StoreSettings")]
public partial class StoreSettings
{
    [PrimaryKey]
    public Guid Id { get; set; }
    public Guid StoreId { get; set; }
    [HasOne]
    public Store Store { get; set; } = null!;
    [MaxLength(3)]
    [DefaultValue(Value = "USD")]
    public string Currency { get; set; } = "USD";
    [MaxLength(50)]
    [DefaultValue(Value = "en-US")]
    public string Locale { get; set; } = "en-US";
    [DefaultValue(Value = "0.00")]
    [Precision(5, 2)]
    public decimal TaxRate { get; set; }
    public bool AcceptsCreditCards { get; set; }
    public bool AcceptsBankTransfers { get; set; }
}

Product Aggregate

[AggregateRoot("Product")]
[Table("Products")]
[Timestampable]
[SoftDeletable]
[Sluggable("Name")]
public partial class Product
{
    [PrimaryKey]
    public Guid Id { get; set; }
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";
    [MaxLength(2000)]
    public string? Description { get; set; }
    [ComplexType]
    public Money Price { get; set; } = new();
    [Required]
    [MaxLength(50)]
    public string Sku { get; set; } = "";
    [DefaultValue(Value = "true")]
    public bool IsActive { get; set; }
    [EnumStorage(AsString = true)]
    public ProductStatus Status { get; set; }
    public Guid StoreId { get; set; }
    [Aggregation]
    [HasOne(WithMany = "Products", ForeignKey = "StoreId")]
    public Store Store { get; set; } = null!;
    [Composition]
    [HasMany(WithOne = "Product", ForeignKey = "ProductId")]
    public List<ProductVariant> Variants { get; set; } = new();
}

[Entity("ProductVariant")]
[Table("ProductVariants")]
public partial class ProductVariant
{
    [PrimaryKey]
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    [HasOne(WithMany = "Variants")]
    public Product Product { get; set; } = null!;
    [Required]
    [MaxLength(100)]
    public string VariantName { get; set; } = "";
    [Required]
    [MaxLength(50)]
    public string Sku { get; set; } = "";
    [Precision(18, 2)]
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

Category Aggregate

[AggregateRoot("Category")]
[Table("Categories")]
[Timestampable]
[Sluggable("Name")]
public partial class Category
{
    [PrimaryKey]
    public int Id { get; set; }
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";
    [MaxLength(500)]
    public string? Description { get; set; }
    public int? ParentId { get; set; }
    [SelfReference(InverseNavigation = "Children", ForeignKey = "ParentId", OnDelete = "Restrict")]
    public Category? Parent { get; set; }
    public List<Category> Children { get; set; } = new();
    public int DisplayOrder { get; set; }
    public int Depth { get; set; }
}

Product-Category Association

[AssociationClass("ProductCategory", typeof(Product), typeof(Category),
    OnDeleteLeft = "Cascade", OnDeleteRight = "Cascade")]
[Table("ProductCategories")]
public partial class ProductCategory
{
    public Guid ProductId { get; set; }
    public int CategoryId { get; set; }
    public Product Product { get; set; } = null!;
    public Category Category { get; set; } = null!;
    public int DisplayOrder { get; set; }
    [DefaultValue(Value = "false")]
    public bool IsFeatured { get; set; }
    [DefaultValue(Sql = "GETUTCDATE()")]
    public DateTimeOffset AddedAt { get; set; }
}

Customer Aggregate

[AggregateRoot("Customer")]
[Table("Customers")]
[Timestampable]
public partial class Customer
{
    [PrimaryKey]
    public Guid Id { get; set; }
    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";
    [Required]
    [MaxLength(254)]
    public string Email { get; set; } = "";
    [MaxLength(20)]
    public string? Phone { get; set; }
    public Address ShippingAddress { get; set; } = null!;
    public Address? BillingAddress { get; set; }
    public List<Order> Orders { get; set; } = new();
}

Order Aggregate

[AggregateRoot("Order")]
[Table("Orders")]
[Timestampable]
[Blameable]
public partial class Order
{
    [PrimaryKey]
    public Guid Id { get; set; }
    [Required]
    [MaxLength(50)]
    public string OrderNumber { get; set; } = "";
    public DateTimeOffset OrderDate { get; set; }
    [Precision(18, 2)]
    public decimal Total { get; set; }
    [EnumStorage(AsString = true)]
    [DefaultValue(Value = "Pending")]
    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();
    public List<Payment> Payments { get; set; } = new();
}

[Entity("OrderItem")]
[Table("OrderItems")]
public partial class OrderItem
{
    [PrimaryKey]
    public int Id { get; set; }
    public Guid OrderId { get; set; }
    [HasOne(WithMany = "Items")]
    public Order Order { get; set; } = null!;
    public Guid ProductId { get; set; }
    [Aggregation]
    [HasOne(ForeignKey = "ProductId")]
    public Product Product { get; set; } = null!;
    public int Quantity { get; set; }
    [Precision(18, 2)]
    public decimal UnitPrice { get; set; }
    [ComputedColumn("[Quantity] * [UnitPrice]", Stored = true)]
    public decimal LineTotal { get; set; }
}

Payment Hierarchy

[Entity("Payment")]
[Table("Payments")]
[Inheritance(Strategy = InheritanceStrategy.TPH, DiscriminatorColumn = "PaymentType")]
public abstract class Payment
{
    [PrimaryKey]
    public Guid Id { get; set; }
    [Required]
    [Precision(18, 2)]
    public decimal Amount { get; set; }
    [Required]
    [MaxLength(3)]
    public string Currency { get; set; } = "USD";
    public Guid OrderId { get; set; }
    [Aggregation]
    [HasOne(WithMany = "Payments", ForeignKey = "OrderId")]
    public Order Order { get; set; } = null!;
    public DateTimeOffset PaidAt { get; set; }
}

[Entity("CreditCardPayment")]
[Inheritance(Strategy = InheritanceStrategy.TPH, DiscriminatorValue = "CreditCard")]
public class CreditCardPayment : Payment
{
    [MaxLength(4)]
    public string CardLastFour { get; set; } = "";
    [MaxLength(20)]
    public string CardBrand { get; set; } = "";
    [MaxLength(50)]
    public string? AuthorizationCode { get; set; }
}

[Entity("BankTransferPayment")]
[Inheritance(Strategy = InheritanceStrategy.TPH, DiscriminatorValue = "BankTransfer")]
public class BankTransferPayment : Payment
{
    [MaxLength(34)]
    public string IBAN { get; set; } = "";
    [MaxLength(11)]
    public string BIC { get; set; } = "";
    [MaxLength(100)]
    public string? TransferReference { get; set; }
}

[Entity("WalletPayment")]
[Inheritance(Strategy = InheritanceStrategy.TPH, DiscriminatorValue = "Wallet")]
public class WalletPayment : Payment
{
    [Required]
    [MaxLength(50)]
    public string WalletProvider { get; set; } = "";
    [Required]
    [MaxLength(100)]
    public string WalletTransactionId { get; set; } = "";
}

Value Types

[Owned]
public class Address
{
    [Required]
    [MaxLength(200)]
    public string Street { get; set; } = "";
    [Required]
    [MaxLength(100)]
    public string City { get; set; } = "";
    [Required]
    [MaxLength(100)]
    public string State { get; set; } = "";
    [Required]
    [MaxLength(20)]
    public string ZipCode { get; set; } = "";
    [Required]
    [MaxLength(100)]
    public string Country { get; set; } = "";
}

[ValueObject("Money")]
public class Money
{
    [Precision(18, 2)]
    public decimal Amount { get; set; }
    [Required]
    [MaxLength(3)]
    [DefaultValue(Value = "USD")]
    public string Currency { get; set; } = "USD";
}

public enum OrderStatus { Pending, Confirmed, Processing, Shipped, Delivered, Cancelled, Refunded }
public enum ProductStatus { Draft, Active, OutOfStock, Discontinued }

The DbContext

[DbContext]
public partial class MarketplaceDbContext : DbContext { }

Generated File Inventory

The source generator produces the following files from the domain above:

Per-Entity Configuration (3 files each)

Entity ConfigurationBase Configuration Registration
Store x x x
StoreSettings x x x
Product x x x
ProductVariant x x x
Category x x x
ProductCategory x x x
Customer x x x
Order x x x
OrderItem x x x
Payment x x x
CreditCardPayment x x x
BankTransferPayment x x x
WalletPayment x x x
Subtotal 39 files

Per-Entity Repository (2 files each, aggregate roots + association class only)

Entity Interface Implementation
Store x x
Product x x
Category x x
ProductCategory x x
Customer x x
Order x x
Payment x x
Subtotal 14 files

DbContext Infrastructure (6 files)

File Description
MarketplaceDbContextBase.g.cs Abstract DbContext with DbSets, hooks
MarketplaceDbContext.g.cs Partial stub
MarketplaceDbContextRegistration.g.cs AddMarketplaceDbContext extension
IMarketplaceDbContextUnitOfWork.g.cs UoW interface
MarketplaceDbContextUnitOfWorkBase.g.cs UoW base with lazy repos
MarketplaceDbContextUnitOfWork.g.cs UoW partial stub
Subtotal 6 files

Behavior Partials

File Description
Product.Behaviors.g.cs CreatedAt, UpdatedAt, IsDeleted, DeletedAt, Slug
Order.Behaviors.g.cs CreatedAt, UpdatedAt, CreatedBy, UpdatedBy
Store.Behaviors.g.cs CreatedAt, UpdatedAt, RowVersion
Customer.Behaviors.g.cs CreatedAt, UpdatedAt
Category.Behaviors.g.cs CreatedAt, UpdatedAt, Slug
Subtotal 5 files

Association Navigation Partials

File Description
Product.AssociationNavigations.g.cs Categories, ProductCategories
Category.AssociationNavigations.g.cs Products, ProductCategories
Subtotal 2 files

Grand Total

Category Files
Configuration 39
Repository 14
DbContext + UoW 6
Behavior partials 5
Association partials 2
Total Generated 66 files

The Numbers

Metric Developer Generator
Entity files 14
DbContext file 1
Enum files 2
Value type files 2
Lines of code ~210 ~2,800
Generated files 66
DI registration lines 2

Amplification factor: ~13x. The developer writes 210 lines of attributed domain code. The compiler generates 2,800 lines of EF Core infrastructure across 66 files.


The Complete UnitOfWork

// Generated: IMarketplaceDbContextUnitOfWork.g.cs
public interface IMarketplaceDbContextUnitOfWork
    : global::FrenchExDev.Net.Entity.Dsl.Abstractions.IUnitOfWork<global::Marketplace.Domain.MarketplaceDbContext>
{
    global::Marketplace.Domain.Repositories.IStoreRepository Stores { get; }
    global::Marketplace.Domain.Repositories.IProductRepository Products { get; }
    global::Marketplace.Domain.Repositories.ICategoryRepository Categories { get; }
    global::Marketplace.Domain.Repositories.IProductCategoryRepository ProductCategories { get; }
    global::Marketplace.Domain.Repositories.ICustomerRepository Customers { get; }
    global::Marketplace.Domain.Repositories.IOrderRepository Orders { get; }
    global::Marketplace.Domain.Repositories.IPaymentRepository Payments { get; }
}

Seven repository properties. All lazily initialized. All typed. All injectable.


DI Wiring

var builder = WebApplication.CreateBuilder(args);

// Register DbContext
builder.Services.AddMarketplaceDbContext(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Marketplace")));

// Register all [Injectable] services (repositories, UoW, listeners)
builder.Services.AddMarketplaceDomainInjectables();

// Register application services
builder.Services.AddScoped<ICurrentUserProvider, HttpContextUserProvider>();

var app = builder.Build();

Three lines. The entire marketplace infrastructure — DbContext, 7 repositories, UnitOfWork, behavior hooks, entity listeners — is wired and ready.


Creating a Store with Products

public class StoreService
{
    private readonly IMarketplaceDbContextUnitOfWork _uow;

    public StoreService(IMarketplaceDbContextUnitOfWork uow) => _uow = uow;

    public async Task<Store> CreateStoreAsync(string name, string description)
    {
        var store = new Store
        {
            Name = name,
            Description = description,
            Settings = new StoreSettings
            {
                Currency = "USD",
                Locale = "en-US",
                TaxRate = 8.25m,
                AcceptsCreditCards = true,
                AcceptsBankTransfers = true
            },
            Address = new Address
            {
                Street = "123 Market St",
                City = "San Francisco",
                State = "CA",
                ZipCode = "94105",
                Country = "US"
            }
        };

        _uow.Stores.Add(store);
        await _uow.SaveChangesAsync();
        // CreatedAt/UpdatedAt auto-stamped by [Timestampable]
        // RowVersion auto-managed by [Versionable]
        return store;
    }
}

Placing an Order

public class OrderService
{
    private readonly IMarketplaceDbContextUnitOfWork _uow;

    public OrderService(IMarketplaceDbContextUnitOfWork uow) => _uow = uow;

    public async Task<Order> PlaceOrderAsync(
        Guid customerId, List<(Guid ProductId, int Quantity)> items)
    {
        var order = new Order
        {
            OrderNumber = $"ORD-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString()[..8]}",
            OrderDate = DateTimeOffset.UtcNow,
            CustomerId = customerId,
            Status = OrderStatus.Pending
        };

        foreach (var (productId, quantity) in items)
        {
            var product = await _uow.Products.FindByIdAsync(productId);
            order.Items.Add(new OrderItem
            {
                ProductId = productId,
                Quantity = quantity,
                UnitPrice = product!.Price.Amount
                // LineTotal auto-computed by [ComputedColumn]
            });
        }

        order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);

        _uow.Orders.Add(order);
        await _uow.SaveChangesAsync();
        // CreatedAt/UpdatedAt auto-stamped by [Timestampable]
        // CreatedBy/UpdatedBy auto-stamped by [Blameable]
        return order;
    }
}

Processing a Payment

public class PaymentService
{
    private readonly IMarketplaceDbContextUnitOfWork _uow;

    public PaymentService(IMarketplaceDbContextUnitOfWork uow) => _uow = uow;

    public async Task<Payment> ProcessCreditCardAsync(
        Guid orderId, decimal amount, string cardLastFour, string cardBrand)
    {
        var payment = new CreditCardPayment
        {
            OrderId = orderId,
            Amount = amount,
            Currency = "USD",
            PaidAt = DateTimeOffset.UtcNow,
            CardLastFour = cardLastFour,
            CardBrand = cardBrand,
            AuthorizationCode = $"AUTH-{Guid.NewGuid().ToString()[..8]}"
        };

        _uow.Payments.Add(payment);

        // Update order status
        var order = await _uow.Orders.FindByIdAsync(orderId);
        order!.Status = OrderStatus.Confirmed;

        await _uow.SaveChangesAsync();
        return payment;
    }
}

Browsing the Catalog

public class CatalogService
{
    private readonly IMarketplaceDbContextUnitOfWork _uow;

    public CatalogService(IMarketplaceDbContextUnitOfWork uow) => _uow = uow;

    public async Task<IReadOnlyList<Product>> GetFeaturedInCategoryAsync(int categoryId)
    {
        // Uses the association repository's bidirectional query
        var productCategories = await _uow.ProductCategories.Query
            .Where(pc => pc.CategoryId == categoryId && pc.IsFeatured)
            .OrderBy(pc => pc.DisplayOrder)
            .Include(pc => pc.Product)
                .ThenInclude(p => p.Variants)
            .ToListAsync();

        return productCategories.Select(pc => pc.Product).ToList();
        // Soft-deleted products are automatically excluded by [SoftDeletable] query filter
    }

    public async Task<IReadOnlyList<Category>> GetCategoryTreeAsync()
    {
        return await _uow.Categories.Query
            .Where(c => c.ParentId == null)
            .Include(c => c.Children)
                .ThenInclude(c => c.Children)
            .OrderBy(c => c.DisplayOrder)
            .ToListAsync();
    }
}

Project Structure

What the developer's project looks like:

Marketplace.Domain/
├── Marketplace.Domain.csproj
├── Entities/
│   ├── Store.cs                    (developer)
│   ├── StoreSettings.cs            (developer)
│   ├── Product.cs                  (developer)
│   ├── ProductVariant.cs           (developer)
│   ├── Category.cs                 (developer)
│   ├── ProductCategory.cs          (developer)
│   ├── Customer.cs                 (developer)
│   ├── Order.cs                    (developer)
│   ├── OrderItem.cs                (developer)
│   ├── Payment.cs                  (developer)
│   ├── CreditCardPayment.cs        (developer)
│   ├── BankTransferPayment.cs       (developer)
│   └── WalletPayment.cs            (developer)
├── ValueTypes/
│   ├── Address.cs                  (developer)
│   ├── Money.cs                    (developer)
│   ├── OrderStatus.cs              (developer)
│   └── ProductStatus.cs            (developer)
├── MarketplaceDbContext.cs          (developer — 2 lines)
├── Configuration/
│   ├── ProductConfiguration.cs     (developer — optional overrides)
│   └── OrderConfiguration.cs       (developer — optional overrides)
├── Repositories/
│   ├── OrderRepository.cs          (developer — custom query methods)
│   ├── IOrderRepository.cs         (developer — custom interface methods)
│   └── CategoryRepository.cs       (developer — tree queries)
└── obj/GeneratedFiles/             (66 generated .g.cs files — not checked in)

The developer writes the domain model and optional customizations. The generated files live in obj/ and are never checked into source control. They are regenerated on every build.


What We Built

Over the course of this series, we modeled a complete online marketplace with:

  • 7 aggregate boundaries (Store, Product, Category, ProductCategory, Customer, Order, Payment)
  • 3 relationship types (Composition, Aggregation, Association)
  • 2 value object strategies (Owned, ComplexType)
  • 1 association class with payload and bidirectional queries
  • 1 self-referencing tree (Category hierarchy)
  • 5 behaviors (Timestampable, SoftDeletable, Blameable, Sluggable, Versionable)
  • 1 inheritance hierarchy (Payment with TPH)
  • Computed columns, enum storage, default values, database comments
  • Custom repositories with domain-specific queries
  • Specification pattern for reusable query criteria

All expressed with attributes. All backed by generated infrastructure. All customizable via the Generation Gap pattern.

In the final part, we compare Entity.Dsl against the alternatives and answer the question: when should you use it, and when should you not?