CodeWizard.Foundation.Domain
1.0.0-ci.131
dotnet add package CodeWizard.Foundation.Domain --version 1.0.0-ci.131
NuGet\Install-Package CodeWizard.Foundation.Domain -Version 1.0.0-ci.131
<PackageReference Include="CodeWizard.Foundation.Domain" Version="1.0.0-ci.131" />
<PackageVersion Include="CodeWizard.Foundation.Domain" Version="1.0.0-ci.131" />
<PackageReference Include="CodeWizard.Foundation.Domain" />
paket add CodeWizard.Foundation.Domain --version 1.0.0-ci.131
#r "nuget: CodeWizard.Foundation.Domain, 1.0.0-ci.131"
#:package CodeWizard.Foundation.Domain@1.0.0-ci.131
#addin nuget:?package=CodeWizard.Foundation.Domain&version=1.0.0-ci.131&prerelease
#tool nuget:?package=CodeWizard.Foundation.Domain&version=1.0.0-ci.131&prerelease
π§± CodeWizard.Foundation.Domain
Foundational domain abstractions for building Clean Architecture solutions in .NET
Part of the CodeWizard.Foundation ecosystem β a curated, opinionated boilerplate for rapidly bootstrapping enterprise-grade applications with Clean Architecture and Vertical Slice Architecture patterns.
π― What is CodeWizard.Foundation.Domain?
CodeWizard.Foundation.Domain provides the essential building blocks for your domain layer:
- Immutable entities with built-in auditing and multi-tenancy
- Strongly-typed value objects (TenantId, HtmlColor)
- Framework-agnostic contracts β works with EF Core, Dapper, MongoDB, CosmosDB
- Zero external dependencies β pure domain logic
- Production-ready β used in real-world enterprise applications
Key Design Principles
β
Clean Architecture aligned β Domain layer is independent and pure
β
Immutable by default β Thread-safe, predictable behavior
β
Multi-tenant ready β Every entity has a TenantId from day one
β
Type-safe β Strong typing prevents common mistakes
β
Modern C# β Leverages C# 12+ features (records, required, init)
π¦ Installation
Via .NET CLI
dotnet add package CodeWizard.Foundation.Domain
Via NuGet Package Manager
Install-Package CodeWizard.Foundation.Domain
Via PackageReference
<PackageReference Include="CodeWizard.Foundation.Domain" Version="1.0.0" />
Requirements
- .NET 8.0 or higher (tested on .NET 10)
- C# 12+ compiler support
π Quick Start
1. Define Your Domain Entity
using CodeWizard.Foundation.Domain;
using CodeWizard.Foundation.Domain.ValueObjects;
// Simple product entity
public record Product : Entity<Guid>
{
public required string Name { get; init; }
public required decimal Price { get; init; }
public string? Description { get; init; }
}
// Usage
var product = new Product
{
Id = Guid.NewGuid(),
TenantId = TenantId.New(),
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "system",
Name = "Gaming Laptop",
Price = 1499.99m,
Description = "High-performance gaming laptop"
};
2. Immutable Updates with with Expressions
// Update price (creates a new instance)
var updatedProduct = product with { Price = 1299.99m };
// Track who modified it
var auditedProduct = updatedProduct.WithUpdated("user@example.com", DateTimeOffset.UtcNow);
3. Entity Equality Based on Identity
var product1 = new Product { Id = Guid.NewGuid(), /* ... */ };
var product2 = new Product { Id = product1.Id, /* ... */ };
// True - equality based on Id and Type
bool areEqual = product1 == product2;
π§© Core Components
π Abstractions (Interfaces)
IEntity<T>
Base contract for all domain entities with a strongly-typed identifier.
public interface IEntity<T> where T : IEquatable<T>
{
T Id { get; init; }
}
IAuditable
Adds creation and modification tracking (all timestamps in UTC).
public interface IAuditable
{
DateTimeOffset CreatedAt { get; init; }
string CreatedBy { get; init; }
DateTimeOffset? UpdatedAt { get; }
string? UpdatedBy { get; }
}
ITenantScoped
Enables multi-tenancy support.
public interface ITenantScoped
{
TenantId TenantId { get; init; }
}
ISoftDeletable
Support for soft deletion (mark as deleted without physical removal).
public interface ISoftDeletable
{
bool IsDeleted { get; }
DateTimeOffset? DeletedAt { get; }
string? DeletedBy { get; }
}
ποΈ Base Entity
Entity<T>
Immutable base record for all domain entities.
public abstract record Entity<T> : IEntity<T>, IAuditable, ITenantScoped
where T : notnull, IEquatable<T>
{
public required T Id { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public required string CreatedBy { get; init; }
public DateTimeOffset? UpdatedAt { get; private set; }
public string? UpdatedBy { get; private set; }
public required TenantId TenantId { get; init; }
// Creates a new instance with updated audit information
public Entity<T> WithUpdated(string updatedBy, DateTimeOffset updatedAt);
// Equality based on Type + Id
public virtual bool Equals(Entity<T>? other);
public override int GetHashCode();
}
Key Features:
- β
Immutable β Use
withexpressions for modifications - β
Value-based equality β Compares by
TypeandId - β Thread-safe β No mutable state
- β Audit tracking β Automatic creation and modification metadata
Example:
public record Order : Entity<Guid>
{
public required string OrderNumber { get; init; }
public required decimal TotalAmount { get; init; }
public required OrderStatus Status { get; init; }
}
// Create
var order = new Order
{
Id = Guid.NewGuid(),
TenantId = TenantId.New(),
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "user@example.com",
OrderNumber = "ORD-2024-001",
TotalAmount = 299.99m,
Status = OrderStatus.Pending
};
// Update status
var completedOrder = order with { Status = OrderStatus.Completed };
// Track modification
var auditedOrder = completedOrder.WithUpdated("admin@example.com", DateTimeOffset.UtcNow);
π Lookup Entity
LookupEntity<T>
Specialized entity for reference data (dropdown values, enumerations stored in DB).
public record LookupEntity<T> : Entity<T>
where T : notnull, IEquatable<T>
{
public required string Code { get; init; } // Stable programmatic identifier
public required string Label { get; init; } // Display label
public string? LocalizationKey { get; init; } // i18n key for front-end
public HtmlColor? Color { get; init; } // Display color
public int Order { get; init; } = 0; // Sorting order
}
Use Cases:
- Order statuses (
PENDING,COMPLETED,CANCELLED) - User roles (
ADMIN,USER,GUEST) - Product categories
- Country/region lists
Example:
public record OrderStatus : LookupEntity<int>
{
public static OrderStatus Pending => new()
{
Id = 1,
TenantId = TenantId.System,
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "system",
Code = "PENDING",
Label = "Pending",
LocalizationKey = "order.status.pending",
Color = HtmlColor.FromString("#FFA500"),
Order = 1
};
public static OrderStatus Completed => new()
{
Id = 2,
TenantId = TenantId.System,
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "system",
Code = "COMPLETED",
Label = "Completed",
LocalizationKey = "order.status.completed",
Color = HtmlColor.FromString("#28A745"),
Order = 2
};
}
π Value Objects
TenantId
Strongly-typed tenant identifier.
public readonly record struct TenantId(Guid Value)
{
public static TenantId New(); // Create new unique ID
public static readonly TenantId System; // System tenant (Guid.Empty)
public static bool TryParse(string s, out TenantId id);
public static implicit operator Guid(TenantId id); // Convert to Guid
public static explicit operator TenantId(Guid value); // Convert from Guid
}
Example:
// Multi-tenant application
var tenantA = TenantId.New();
var tenantB = TenantId.New();
// Single-tenant application
var tenant = TenantId.System;
// Parse from string
if (TenantId.TryParse("550e8400-e29b-41d4-a716-446655440000", out var parsed))
{
Console.WriteLine($"Parsed: {parsed}");
}
// Implicit conversion to Guid
Guid guid = tenantA; // No cast needed
HtmlColor
Validated HTML/CSS color value.
public readonly record struct HtmlColor
{
public string Value { get; }
public static readonly HtmlColor Transparent;
public static HtmlColor FromString(string value);
public static bool TryParse(string? value, out HtmlColor color);
}
Supported Formats:
- Short hex:
#FFF - Standard hex:
#FFFFFF - Hex with alpha:
#FFFFFF80 - Named color:
transparent
Example:
// Create from valid hex
var red = HtmlColor.FromString("#FF0000");
var green = HtmlColor.FromString("#0F0");
var semiTransparent = HtmlColor.FromString("#FF000080");
// Named color
var transparent = HtmlColor.Transparent;
// Safe parsing
if (HtmlColor.TryParse("#INVALID", out var color))
{
// Won't execute
}
else
{
Console.WriteLine("Invalid color");
}
// Throws on invalid format
try
{
var invalid = HtmlColor.FromString("not-a-color"); // ArgumentException
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
π Multi-Tenancy Support
Every entity is tenant-scoped by default via the TenantId property.
Multi-Tenant Application
var tenantA = TenantId.New();
var tenantB = TenantId.New();
var productA = new Product
{
Id = Guid.NewGuid(),
TenantId = tenantA, // Belongs to tenant A
/* ... */
};
var productB = new Product
{
Id = Guid.NewGuid(),
TenantId = tenantB, // Belongs to tenant B
/* ... */
};
// Different tenants, different products
bool areSame = productA == productB; // False
Single-Tenant Application
// Use the System tenant for all entities
var product = new Product
{
Id = Guid.NewGuid(),
TenantId = TenantId.System,
/* ... */
};
π Implementing Soft Deletion
public record SoftDeletableProduct : Entity<Guid>, ISoftDeletable
{
public required string Name { get; init; }
public required decimal Price { get; init; }
// ISoftDeletable implementation
public bool IsDeleted { get; init; }
public DateTimeOffset? DeletedAt { get; init; }
public string? DeletedBy { get; init; }
}
// Usage
var product = new SoftDeletableProduct
{
Id = Guid.NewGuid(),
TenantId = TenantId.System,
CreatedAt = DateTimeOffset.UtcNow,
CreatedBy = "user@example.com",
Name = "Gaming Mouse",
Price = 79.99m,
IsDeleted = false
};
// Mark as deleted (creates new instance)
var deletedProduct = product with
{
IsDeleted = true,
DeletedAt = DateTimeOffset.UtcNow,
DeletedBy = "admin@example.com"
};
π¨ Advanced Patterns
Strongly-Typed IDs
// Define strongly-typed IDs
public readonly record struct ProductId(Guid Value);
public readonly record struct CustomerId(Guid Value);
public record Product : Entity<ProductId>
{
public required string Name { get; init; }
}
public record Customer : Entity<CustomerId>
{
public required string Email { get; init; }
}
// Type safety prevents mistakes
var product = new Product { Id = new ProductId(Guid.NewGuid()), /* ... */ };
var customer = new Customer { Id = new CustomerId(Guid.NewGuid()), /* ... */ };
// Compile error: can't assign ProductId to CustomerId
// customer = customer with { Id = product.Id }; // β Won't compile
Composite Entities
public record Order : Entity<Guid>
{
public required string OrderNumber { get; init; }
public required CustomerId CustomerId { get; init; }
public required IReadOnlyList<OrderLine> Lines { get; init; }
}
public record OrderLine // Not an entity, just a value object
{
public required ProductId ProductId { get; init; }
public required int Quantity { get; init; }
public required decimal UnitPrice { get; init; }
}
βοΈ Persistence Integration Examples
Entity Framework Core
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
// Convert TenantId to Guid for storage
entity.Property(e => e.TenantId)
.HasConversion(
v => v.Value,
v => new TenantId(v));
// Global query filter for multi-tenancy
entity.HasQueryFilter(e => e.TenantId == currentTenantId);
});
}
}
Dapper
public class ProductRepository
{
public async Task<Product?> GetByIdAsync(Guid id, TenantId tenantId)
{
const string sql = @"
SELECT Id, TenantId, CreatedAt, CreatedBy, UpdatedAt, UpdatedBy,
Name, Price, Description
FROM Products
WHERE Id = @Id AND TenantId = @TenantId";
var result = await connection.QuerySingleOrDefaultAsync<ProductDto>(sql,
new { Id = id, TenantId = (Guid)tenantId });
return result is null ? null : MapToEntity(result);
}
}
MongoDB
public class ProductRepository
{
private readonly IMongoCollection<Product> _collection;
public async Task<Product?> GetByIdAsync(Guid id, TenantId tenantId)
{
var filter = Builders<Product>.Filter.And(
Builders<Product>.Filter.Eq(p => p.Id, id),
Builders<Product>.Filter.Eq(p => p.TenantId, tenantId));
return await _collection.Find(filter).FirstOrDefaultAsync();
}
}
π Best Practices
β DO
Use
withexpressions for modificationsvar updated = product with { Price = 99.99m };Call
WithUpdated()when modifying entitiesvar audited = updated.WithUpdated("user@example.com", DateTimeOffset.UtcNow);Use strongly-typed IDs for type safety
public readonly record struct ProductId(Guid Value);Store timestamps in UTC
CreatedAt = DateTimeOffset.UtcNowValidate in constructors or factory methods
public static Product Create(string name, decimal price) { ArgumentException.ThrowIfNullOrWhiteSpace(name); if (price <= 0) throw new ArgumentException("Price must be positive"); // ... }
β DON'T
Don't mutate entities directly (they're immutable)
// β Won't compile product.Price = 99.99m;Don't use
Guid.Emptyfor TenantId (useTenantId.System)// β Avoid TenantId = new TenantId(Guid.Empty) // β Use TenantId = TenantId.SystemDon't forget to set
requiredproperties// β Compile error var product = new Product(); // β Set all required properties var product = new Product { Id = ..., TenantId = ..., /* ... */ };
π§ͺ Testing
[Fact]
public void Product_Equality_ShouldBeBasedOnIdAndType()
{
// Arrange
var id = Guid.NewGuid();
var product1 = CreateProduct(id, "Product A", 100m);
var product2 = CreateProduct(id, "Product B", 200m);
// Act & Assert
Assert.Equal(product1, product2); // Same ID = equal
Assert.Equal(product1.GetHashCode(), product2.GetHashCode());
}
[Fact]
public void Entity_WithUpdated_ShouldCreateNewInstance()
{
// Arrange
var product = CreateProduct(Guid.NewGuid(), "Product", 100m);
var updatedBy = "user@example.com";
var updatedAt = DateTimeOffset.UtcNow;
// Act
var updated = product.WithUpdated(updatedBy, updatedAt);
// Assert
Assert.NotSame(product, updated); // Different instances
Assert.Equal(updatedBy, updated.UpdatedBy);
Assert.Equal(updatedAt, updated.UpdatedAt);
}
πΊοΈ Roadmap
- Domain Events β Event-driven architecture support
- Aggregate Root β Separate base class for aggregates
- Specification Pattern β Reusable query logic
- Result Pattern β Railway-oriented programming
- Source Generators β Auto-generate strongly-typed IDs
- Versioning/Concurrency β Optimistic locking support
π Related Packages
| Package | Purpose |
|---|---|
CodeWizard.Foundation.Persistence |
Generic repository and CQRS abstractions |
CodeWizard.Foundation.Application |
MediatR-based interactors and use cases |
CodeWizard.Foundation.Infrastructure |
External integrations and anti-corruption layer |
CodeWizard.Foundation.Adapter |
Minimal API adapters and presenters |
π€ Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request with tests
π License
This project is licensed under the MIT License β see the LICENSE file for details.
π§ββοΈ About Code Wizard
Code Wizard is a .NET consultancy focused on Clean Architecture, Domain-Driven Design, and modern software craftsmanship.
Created by Michele Panipucci.
"Write less boilerplate. Build more value."
π Links
- π¦ NuGet Package
- π Full Documentation
- π Report Issues
- π¬ Discussions
Happy coding! π
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on CodeWizard.Foundation.Domain:
| Package | Downloads |
|---|---|
|
CodeWizard.Foundation.Application
CodeWizard.Foundation.Application provides CQRS building blocks (ICommand, IQuery, ICommandHandler, IQueryHandler) and a flexible Result<T> pattern with ValidationError for clean application layer design. |
|
|
CodeWizard.Foundation.Persistence.EntityFramework
Entity Framework Core persistence layer for CodeWizard.Foundation with SQL Server support |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-ci.131 | 227 | 12/17/2025 |
| 1.0.0-ci.129 | 220 | 12/17/2025 |