Code-First Walkthrough
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
What You'll Build
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>
Prerequisites
- .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)
Step 1: Create the Project
Create a new console application for our e-commerce domain:
mkdir JD.Domain.Tutorial.CodeFirst
cd JD.Domain.Tutorial.CodeFirst
dotnet new console
Step 2: Install Required Packages
Install 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.Generator
Step 3: Define Domain Entities with Attributes
Create entity classes with JD.Domain attributes and data annotations for automatic manifest generation.
3a. Configure Assembly-Level Manifest
Create Properties/AssemblyInfo.cs:
using JD.Domain.ManifestGeneration;
[assembly: GenerateManifest("ECommerce", Version = "1.0.0")]
3b. Create Entity Classes
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
}
Explanation
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.
Step 4: Build and Verify Manifest Generation
Build your project to trigger the source generator:
dotnet build
The ManifestSourceGenerator will automatically create a static class named ECommerceManifest with a GeneratedManifest property containing your complete domain manifest.
Verify Generated 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!
Step 5: Define Business Rules
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();
}
}
Explanation
- 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.)
Step 6: Create the DbContext
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);
}
}
Explanation
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).
Step 7: Create the Validation Service
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));
}
}
Explanation
DomainRuntime.CreateEngine()creates a rule evaluation engineengine.Evaluate()runs rules against an entityResult<T>is a functional programming pattern that represents success or failure
Step 8: Test the Domain
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
Expected Output
=== 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
Step 9: Generate Database Schema (Optional)
If you want to create the database, add migrations:
dotnet ef migrations add InitialCreate
dotnet ef database update
This 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)
Step 10: Explore Generated Domain Types
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 # Windows
You'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
Using Generated Types
// 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);
What You've Learned
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
Key Concepts
1. Single Source of Truth
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)
2. Opt-In Architecture
Your entities remain POCOs. No forced inheritance, no marker interfaces. This makes JD.Domain easy to adopt incrementally.
3. Declarative Rules
Rules describe what should be true, not how to validate. This makes them:
- Easy to read and understand
- Reusable across contexts
- Testable in isolation
4. Result Pattern
Result<T> eliminates exceptions for expected failures (validation errors) while maintaining type safety.
Next Steps
Extend Your Domain
- Add more entities (Product, Category, etc.)
- Add value objects (Address, Money, Email)
- Define relationships and navigation properties
Add More Rules
- Create Validator rules (context-dependent validation)
- Create Policy rules (authorization)
- Add Derivation rules (computed properties)
- Compose rules with
.Include()and.When()
Integrate with ASP.NET Core
Follow the ASP.NET Core Integration Tutorial to add automatic API validation.
Generate FluentValidation Validators
dotnet add package JD.Domain.FluentValidation.Generator
See Source Generators Tutorial for details.
Track Domain Evolution
Use snapshots to track changes over time:
dotnet tool install -g JD.Domain.Cli
jd-domain snapshot --manifest domain.json --output ./snapshots
See Version Management Tutorial for details.
Additional Resources
- 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
Get Help
- 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.