CodeWizard.Foundation.Domain 1.0.0-ci.131

This is a prerelease version of CodeWizard.Foundation.Domain.
dotnet add package CodeWizard.Foundation.Domain --version 1.0.0-ci.131
                    
NuGet\Install-Package CodeWizard.Foundation.Domain -Version 1.0.0-ci.131
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeWizard.Foundation.Domain" Version="1.0.0-ci.131" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeWizard.Foundation.Domain" Version="1.0.0-ci.131" />
                    
Directory.Packages.props
<PackageReference Include="CodeWizard.Foundation.Domain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeWizard.Foundation.Domain --version 1.0.0-ci.131
                    
#r "nuget: CodeWizard.Foundation.Domain, 1.0.0-ci.131"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeWizard.Foundation.Domain@1.0.0-ci.131
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeWizard.Foundation.Domain&version=1.0.0-ci.131&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeWizard.Foundation.Domain&version=1.0.0-ci.131&prerelease
                    
Install as a Cake Tool

🧱 CodeWizard.Foundation.Domain

NuGet License: MIT .NET

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 with expressions for modifications
  • βœ… Value-based equality β€” Compares by Type and Id
  • βœ… 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 with expressions for modifications

    var updated = product with { Price = 99.99m };
    
  • Call WithUpdated() when modifying entities

    var 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.UtcNow
    
  • Validate 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.Empty for TenantId (use TenantId.System)

    // ❌ Avoid
    TenantId = new TenantId(Guid.Empty)
    
    // βœ… Use
    TenantId = TenantId.System
    
  • Don't forget to set required properties

    // ❌ 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

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:

  1. Fork the repository
  2. Create a feature branch
  3. 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."



Happy coding! πŸš€

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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