In this tutorial, you'll build a complete e-commerce domain from scratch using JD.Domain's code-first approach. You'll learn how to define entities, configure properties, add business rules, integrate with EF Core, and generate rich domain types.
Time: 45-60 minutes | Level: Beginner
By the end of this tutorial, you'll have:
- ✅ A complete domain model with Customer and Order entities
- ✅ Business rules with invariants and validators
- ✅ EF Core integration with auto-generated configurations
- ✅ A runtime validation engine
- ✅ Rich domain types with construction safety using
Result<T>
- .NET 10.0 SDK or later
- Basic understanding of C# and Entity Framework Core
- A code editor (Visual Studio, VS Code, or Rider)
- SQL Server LocalDB or another database (optional, can use in-memory)
Create a new console application for our e-commerce domain:
mkdir JD.Domain.Tutorial.CodeFirst
cd JD.Domain.Tutorial.CodeFirst
dotnet new consoleInstall the JD.Domain packages for code-first development:
# Core packages
dotnet add package JD.Domain.Abstractions
dotnet add package JD.Domain.Rules
dotnet add package JD.Domain.Runtime
# Automatic manifest generation (source generators)
dotnet add package JD.Domain.ManifestGeneration
dotnet add package JD.Domain.ManifestGeneration.Generator
# EF Core integration
dotnet add package JD.Domain.EFCore
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
# Source generator for rich domain types
dotnet add package JD.Domain.DomainModel.GeneratorCreate entity classes with JD.Domain attributes and data annotations for automatic manifest generation.
Create Properties/AssemblyInfo.cs:
using JD.Domain.ManifestGeneration;
[assembly: GenerateManifest("ECommerce", Version = "1.0.0")]Create Entities/Customer.cs:
using System.ComponentModel.DataAnnotations;
using JD.Domain.ManifestGeneration;
namespace JD.Domain.Tutorial.CodeFirst.Entities;
[DomainEntity(TableName = "Customers", Schema = "dbo")]
public class Customer
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string Email { get; set; } = string.Empty;
[MaxLength(20)]
public string? Phone { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; } = true;
// Navigation property (excluded from manifest automatically)
[ExcludeFromManifest]
public ICollection<Order> Orders { get; set; } = new List<Order>();
}Create Entities/Order.cs:
using System.ComponentModel.DataAnnotations;
using JD.Domain.ManifestGeneration;
namespace JD.Domain.Tutorial.CodeFirst.Entities;
[DomainEntity(TableName = "Orders", Schema = "dbo")]
public class Order
{
[Key]
public int Id { get; set; }
[Required]
public int CustomerId { get; set; }
[Required]
[MaxLength(50)]
public string OrderNumber { get; set; } = string.Empty;
[Required]
public decimal TotalAmount { get; set; }
public DateTime OrderDate { get; set; }
public OrderStatus Status { get; set; }
// Navigation property (excluded from manifest)
[ExcludeFromManifest]
public Customer? Customer { get; set; }
}
public enum OrderStatus
{
Pending = 0,
Processing = 1,
Shipped = 2,
Delivered = 3,
Cancelled = 4
}NO MANUAL STRING WRITING! The ManifestSourceGenerator automatically:
- Extracts all property names and types from your code
- Reads data annotations ([Key], [Required], [MaxLength])
- Detects nullability from nullable reference types (string? vs string)
- Generates table/schema configuration from [DomainEntity] attribute
- Creates a complete
DomainManifestat compile-time
Your entities remain simple POCOs with standard data annotations - no forced inheritance or special interfaces required.
Build your project to trigger the source generator:
dotnet buildThe ManifestSourceGenerator will automatically create a static class named ECommerceManifest with a GeneratedManifest property containing your complete domain manifest.
You can inspect the generated manifest (optional):
# View generated files (Windows)
dir obj\Debug\net10.0\generated /s /b | findstr ECommerceManifest
# View generated files (Linux/Mac)
find obj/Debug/net10.0/generated -name "*ECommerceManifest*"The generated manifest will look like:
// Auto-generated by JD.Domain.ManifestGeneration.Generator
namespace JD.Domain.Generated;
public static class ECommerceManifest
{
public static DomainManifest GeneratedManifest { get; } = new()
{
Name = "ECommerce",
Version = new Version("1.0.0"),
Entities = new List<EntityManifest>
{
new EntityManifest
{
Name = "Customer",
TableName = "Customers",
Schema = "dbo",
Properties = new List<PropertyManifest>
{
new PropertyManifest { Name = "Id", TypeName = "System.Int32", IsRequired = true },
new PropertyManifest { Name = "Name", TypeName = "System.String", IsRequired = true, MaxLength = 100 },
new PropertyManifest { Name = "Email", TypeName = "System.String", IsRequired = true, MaxLength = 255 },
// ... more properties
},
KeyProperties = new List<string> { "Id" }
},
// ... more entities
}
};
}All metadata extracted automatically from your entity classes - zero manual string writing required!
Create rule sets for validating entities.
Create Domain/CustomerRules.cs:
using JD.Domain.Rules;
using JD.Domain.Tutorial.CodeFirst.Entities;
namespace JD.Domain.Tutorial.CodeFirst.Domain;
public static class CustomerRules
{
public static RuleSetManifest Default()
{
return new RuleSetBuilder<Customer>("Default")
// Name is required and has minimum length
.Invariant("Name.Required", c => !string.IsNullOrWhiteSpace(c.Name))
.WithMessage("Customer name is required")
.Invariant("Name.MinLength", c => c.Name.Length >= 2)
.WithMessage("Customer name must be at least 2 characters")
// Email is required and valid format
.Invariant("Email.Required", c => !string.IsNullOrWhiteSpace(c.Email))
.WithMessage("Customer email is required")
.Invariant("Email.Format", c => c.Email.Contains("@") && c.Email.Contains("."))
.WithMessage("Customer email must be a valid email address")
// Phone format (if provided)
.Invariant("Phone.Format", c => string.IsNullOrEmpty(c.Phone) || c.Phone.Length >= 10)
.WithMessage("Phone number must be at least 10 digits")
// Active customers must have been created
.Invariant("Active.CreatedAt", c => !c.IsActive || c.CreatedAt != default)
.WithMessage("Active customers must have a creation date")
.Build();
}
}Create Domain/OrderRules.cs:
using JD.Domain.Rules;
using JD.Domain.Tutorial.CodeFirst.Entities;
namespace JD.Domain.Tutorial.CodeFirst.Domain;
public static class OrderRules
{
public static RuleSetManifest Default()
{
return new RuleSetBuilder<Order>("Default")
// Order number is required and properly formatted
.Invariant("OrderNumber.Required", o => !string.IsNullOrWhiteSpace(o.OrderNumber))
.WithMessage("Order number is required")
.Invariant("OrderNumber.Format", o => o.OrderNumber.StartsWith("ORD-"))
.WithMessage("Order number must start with 'ORD-'")
// Customer ID must be positive
.Invariant("CustomerId.Positive", o => o.CustomerId > 0)
.WithMessage("Order must be associated with a valid customer")
// Total amount must be positive
.Invariant("TotalAmount.Positive", o => o.TotalAmount > 0)
.WithMessage("Order total must be greater than zero")
// Order date validations
.Invariant("OrderDate.NotFuture", o => o.OrderDate <= DateTime.UtcNow)
.WithMessage("Order date cannot be in the future")
.Invariant("OrderDate.NotTooOld", o => o.OrderDate >= DateTime.UtcNow.AddYears(-10))
.WithMessage("Order date cannot be more than 10 years in the past")
// Status-specific rules
.Invariant("Status.ValidTransition", o =>
o.Status == OrderStatus.Pending ||
o.Status == OrderStatus.Processing ||
o.Status == OrderStatus.Shipped ||
o.Status == OrderStatus.Delivered ||
o.Status == OrderStatus.Cancelled)
.WithMessage("Order status is invalid")
.Build();
}
}- Invariants are always-true rules that define valid entity state
.WithMessage()provides user-friendly error messages- Rules are declarative - you describe what should be true, not how to validate
- Rules are reusable across different contexts (API, domain layer, etc.)
Create an EF Core DbContext that applies the domain manifest.
Create Data/ECommerceDbContext.cs:
using JD.Domain.EFCore;
using JD.Domain.Tutorial.CodeFirst.Domain;
using JD.Domain.Tutorial.CodeFirst.Entities;
using Microsoft.EntityFrameworkCore;
namespace JD.Domain.Tutorial.CodeFirst.Data;
public class ECommerceDbContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
public ECommerceDbContext(DbContextOptions<ECommerceDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply auto-generated domain manifest - this generates all EF Core configurations
modelBuilder.ApplyDomainManifest(ECommerceManifest.GeneratedManifest);
// Optional: Add additional EF-specific configurations not in manifest
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
}
}The ApplyDomainManifest() extension method reads your domain manifest and generates:
- Table names and schemas
- Primary keys
- Indexes (unique and non-unique)
- Property constraints (required, max length, precision)
You can still add additional EF-specific configurations manually (like relationships).
Create a service that validates entities using the domain engine.
Create Services/DomainValidationService.cs:
using JD.Domain.Abstractions;
using JD.Domain.Runtime;
using JD.Domain.Tutorial.CodeFirst.Domain;
namespace JD.Domain.Tutorial.CodeFirst.Services;
public class DomainValidationService
{
private readonly IDomainEngine _engine;
public DomainValidationService()
{
// Use auto-generated manifest
_engine = DomainRuntime.CreateEngine(ECommerceManifest.GeneratedManifest);
}
public Result<T> Validate<T>(T entity, RuleSetManifest ruleSet) where T : class
{
var result = _engine.Evaluate(entity, ruleSet);
if (result.IsValid)
{
return Result<T>.Success(entity);
}
var errors = string.Join("; ", result.Errors.Select(e => e.Message));
return Result<T>.Failure(new DomainError(
"ValidationFailed",
errors,
RuleSeverity.Error));
}
}DomainRuntime.CreateEngine()creates a rule evaluation engineengine.Evaluate()runs rules against an entityResult<T>is a functional programming pattern that represents success or failure
Update Program.cs to test your domain:
using JD.Domain.Tutorial.CodeFirst.Domain;
using JD.Domain.Tutorial.CodeFirst.Entities;
using JD.Domain.Tutorial.CodeFirst.Services;
var validationService = new DomainValidationService();
Console.WriteLine("=== Testing Customer Validation ===\n");
// Test 1: Valid customer
var validCustomer = new Customer
{
Id = 1,
Name = "John Doe",
Email = "john.doe@example.com",
Phone = "555-123-4567",
CreatedAt = DateTime.UtcNow,
IsActive = true
};
var result1 = validationService.Validate(validCustomer, CustomerRules.Default());
Console.WriteLine($"Valid Customer: {(result1.IsSuccess ? "✓ PASSED" : "✗ FAILED")}");
if (!result1.IsSuccess)
{
Console.WriteLine($" Errors: {result1.Error.Message}");
}
// Test 2: Invalid customer (empty name, bad email)
var invalidCustomer = new Customer
{
Id = 2,
Name = "",
Email = "invalid-email",
CreatedAt = DateTime.UtcNow,
IsActive = true
};
var result2 = validationService.Validate(invalidCustomer, CustomerRules.Default());
Console.WriteLine($"\nInvalid Customer: {(result2.IsSuccess ? "✓ PASSED" : "✗ FAILED (Expected)")}");
if (!result2.IsSuccess)
{
Console.WriteLine($" Errors: {result2.Error.Message}");
}
Console.WriteLine("\n=== Testing Order Validation ===\n");
// Test 3: Valid order
var validOrder = new Order
{
Id = 1,
CustomerId = 1,
OrderNumber = "ORD-2025-001",
TotalAmount = 99.99m,
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending
};
var result3 = validationService.Validate(validOrder, OrderRules.Default());
Console.WriteLine($"Valid Order: {(result3.IsSuccess ? "✓ PASSED" : "✗ FAILED")}");
if (!result3.IsSuccess)
{
Console.WriteLine($" Errors: {result3.Error.Message}");
}
// Test 4: Invalid order (bad order number, negative amount, future date)
var invalidOrder = new Order
{
Id = 2,
CustomerId = 0,
OrderNumber = "INVALID",
TotalAmount = -50.00m,
OrderDate = DateTime.UtcNow.AddDays(1),
Status = OrderStatus.Pending
};
var result4 = validationService.Validate(invalidOrder, OrderRules.Default());
Console.WriteLine($"\nInvalid Order: {(result4.IsSuccess ? "✓ PASSED" : "✗ FAILED (Expected)")}");
if (!result4.IsSuccess)
{
Console.WriteLine($" Errors: {result4.Error.Message}");
}Run the application:
dotnet run=== Testing Customer Validation ===
Valid Customer: ✓ PASSED
Invalid Customer: ✗ FAILED (Expected)
Errors: Customer name is required; Customer email must be a valid email address
=== Testing Order Validation ===
Valid Order: ✓ PASSED
Invalid Order: ✗ FAILED (Expected)
Errors: Order number must start with 'ORD-'; Order must be associated with a valid customer; Order total must be greater than zero; Order date cannot be in the future
If you want to create the database, add migrations:
dotnet ef migrations add InitialCreate
dotnet ef database updateThis will create tables with all the configurations from your domain manifest:
Customerstable with unique email indexOrderstable with unique order number index- Proper constraints (required fields, max lengths, precision)
The JD.Domain.DomainModel.Generator package automatically generates construction-safe domain types.
Check your obj/ folder for generated files:
find obj -name "*DomainModel.g.cs" # Linux/Mac
dir obj /s /b | findstr DomainModel.g.cs # WindowsYou'll find generated types like DomainCustomer and DomainOrder with:
- Static
Create()methods returningResult<T> FromEntity()methods to wrap existing entitiesWith*()mutation methods- Automatic rule enforcement
// Construction-safe creation
var result = DomainCustomer.Create(
name: "Jane Doe",
email: "jane@example.com",
phone: "555-987-6543");
if (result.IsSuccess)
{
var customer = result.Value;
Console.WriteLine($"Created customer: {customer.Name}");
}
else
{
Console.WriteLine($"Failed: {result.Error.Message}");
}
// Wrap existing entity
var existingCustomer = new Customer { Name = "John", Email = "john@test.com" };
var domainCustomer = DomainCustomer.FromEntity(existingCustomer);In this tutorial, you:
✅ Defined entities as simple POCOs without inheritance
✅ Used the fluent DSL to describe your domain model
✅ Added EF Core configurations declaratively
✅ Defined business rules as invariants
✅ Created a runtime validation engine
✅ Validated entities and handled Result<T> patterns
✅ Applied domain configurations to EF Core DbContext
✅ Explored auto-generated construction-safe domain types
Your domain manifest is the single source of truth. From it, JD.Domain generates:
- EF Core configurations
- Rich domain types
- FluentValidation validators (if you add that generator)
Your entities remain POCOs. No forced inheritance, no marker interfaces. This makes JD.Domain easy to adopt incrementally.
Rules describe what should be true, not how to validate. This makes them:
- Easy to read and understand
- Reusable across contexts
- Testable in isolation
Result<T> eliminates exceptions for expected failures (validation errors) while maintaining type safety.
- Add more entities (Product, Category, etc.)
- Add value objects (Address, Money, Email)
- Define relationships and navigation properties
- Create Validator rules (context-dependent validation)
- Create Policy rules (authorization)
- Add Derivation rules (computed properties)
- Compose rules with
.Include()and.When()
Follow the ASP.NET Core Integration Tutorial to add automatic API validation.
dotnet add package JD.Domain.FluentValidation.GeneratorSee Source Generators Tutorial for details.
Use snapshots to track changes over time:
dotnet tool install -g JD.Domain.Cli
jd-domain snapshot --manifest domain.json --output ./snapshotsSee Version Management Tutorial for details.
- Domain Modeling Tutorial - Deep dive into modeling DSL
- Business Rules Tutorial - Advanced rule patterns
- EF Core Integration - More EF Core features
- API Reference - Complete API documentation
- Questions? Open a GitHub Issue
- Sample Code See
samples/JD.Domain.Samples.CodeFirst/for a complete working example
Congratulations on completing the Code-First walkthrough! You now have a solid foundation for building rich domain models with JD.Domain.