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; }
}[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; }
}[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; }
}[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; }
}[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();
}[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; }
}[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; } = "";
}[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 }[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 { }[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; }
}// 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();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;
}
}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;
}
}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;
}
}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();
}
}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)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?