Domain-Driven Design (DDD) base library for .NET Standard 2.1 and modern .NET projects.
Goodtocode.Domain provides foundational types for building DDD, clean architecture, and event-driven systems. It includes base classes for domain entities, audit fields, domain events, and secured/multi-tenant entities. The library is lightweight, dependency-free, and designed to work with EF Core, Cosmos DB, Table Storage, or custom repositories.
- Library:
netstandard2.1 - Tests/examples:
net10.0
- Domain entity base with audit fields (
CreatedOn,ModifiedOn,DeletedOn,Timestamp) - Domain event pattern and dispatcher (
IDomainEvent,IDomainHandler,DomainDispatcher) - Equality and identity management for aggregate roots
- Partition key and row key support for document/table stores (
PartitionKeyandRowKeyboth default toId.ToString()unless specified) - Secured entity base for multi-tenancy and ownership (
OwnerId,TenantId,CreatedBy,ModifiedBy,DeletedBy) - Extension methods for authorization and ownership queries
- Invariant state protection for audit and security fields (fields are only set if not already set, ensuring consistency and preventing accidental overwrites)
dotnet add package Goodtocode.Domain- Clone this repository
git clone https://github.com/goodtocode/aspect-domain.git - Build the solution
cd src dotnet build Goodtocode.Domain.sln - Run tests
cd Goodtocode.Domain.Tests dotnet test
DomainEntity<TModel>: Base entity with audit fields (CreatedOn,ModifiedOn,DeletedOn,Timestamp), identity (Id), partition key, row key, and domain event tracking.PartitionKeyandRowKeyboth default toId.ToString()unless explicitly set. This supports portability across Cosmos DB, Table Storage, and other stores. If you do not specify a value, both will be the same by default.SecuredEntity<TModel>: ExtendsDomainEntity<TModel>withOwnerId,TenantId, and audit fields for user actions (CreatedBy,ModifiedBy,DeletedBy).PartitionKeydefaults toTenantId.ToString()for multi-tenant isolation.- Invariant state protection: Methods like
MarkCreated,MarkDeleted, etc. only set fields if not already set, ensuring entity state is consistent and protected from accidental changes. - Domain events: Implement
IDomainEvent<TModel>and dispatch withDomainDispatcher.
using Goodtocode.Domain.Entities;
public sealed class MyEntity : DomainEntity<MyEntity>
{
public string Name { get; private set; } = string.Empty;
public int Value { get; private set; }
private MyEntity() { }
public MyEntity(Guid id, string name, int value) : base(id)
{
Name = name;
Value = value;
}
}// Example: Customizing PartitionKey and RowKey
public sealed class TableEntity : DomainEntity<TableEntity>
{
public TableEntity(Guid id, string partitionKey, string rowKey) : base(id, partitionKey, rowKey) { }
}using Goodtocode.Domain.Entities;
public sealed class Document : SecuredEntity<Document>
{
public string Title { get; private set; } = string.Empty;
private Document() { }
public Document(Guid id, Guid ownerId, Guid tenantId, string title) : base(id, ownerId, tenantId)
{
Title = title;
}
}
// Query helpers
var ownedDocuments = queryableDocuments.WhereOwner(ownerId);
var tenantDocuments = queryableDocuments.WhereTenant(tenantId);
var authorized = queryableDocuments.WhereAuthorized(tenantId, ownerId);using Goodtocode.Domain.Entities;
using Goodtocode.Domain.Events;
public sealed class Person : SecuredEntity<Person>
{
public string Email { get; private set; } = string.Empty;
public Person(Guid id, Guid ownerId, Guid tenantId, string email)
: base(id, ownerId, tenantId)
{
Email = email;
AddDomainEvent(new PersonCreatedEvent(this));
}
}
public sealed class PersonCreatedEvent : IDomainEvent<Person>
{
public Person Item { get; }
public DateTime OccurredOn { get; }
public PersonCreatedEvent(Person person)
{
Item = person;
OccurredOn = DateTime.UtcNow;
}
}
public sealed class PersonCreatedHandler : IDomainHandler<PersonCreatedEvent>
{
public Task HandleAsync(PersonCreatedEvent domainEvent)
{
Console.WriteLine($"Created: {domainEvent.Item.Email}");
return Task.CompletedTask;
}
}
// Dispatcher usage (with your DI container)
var serviceProvider = new ServiceCollection();
serviceProvider.AddTransient<IDomainHandler<PersonCreatedEvent>, PersonCreatedHandler>();
serviceProvider.BuildServiceProvider();
var dispatcher = new DomainDispatcher(serviceProvider);
await dispatcher.DispatchAsync(person.DomainEvents);
person.ClearDomainEvents();To ensure audit and security fields are set correctly and invariant state is protected, you must wire up your DbContext to set these fields during the entity lifecycle.
Example:
public class ExampleDbContext : DbContext
{
private readonly ICurrentUserContext _currentUserContext;
public ExampleDbContext(DbContextOptions options, ICurrentUserContext currentUserContext)
: base(options)
{
_currentUserContext = currentUserContext;
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
SetAuditFields();
SetSecurityFields();
return base.SaveChangesAsync(cancellationToken);
}
private void SetAuditFields()
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is IAuditable auditable)
{
if (entry.State == EntityState.Modified)
{
auditable.MarkModified();
}
else if (entry.State == EntityState.Deleted)
{
auditable.MarkDeleted();
entry.State = EntityState.Modified;
}
}
}
}
private void SetSecurityFields()
{
if (_currentUserContext is null) return;
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is ISecurable securable)
{
if (entry.State == EntityState.Added)
{
if (securable.OwnerId == Guid.Empty)
securable.ChangeOwner(_currentUserContext.OwnerId);
if (securable.TenantId == Guid.Empty)
securable.ChangeTenant(_currentUserContext.TenantId);
}
}
}
}
}Note:
- This pattern should be implemented in your infrastructure layer (not in the domain library).
- See
Goodtocode.Domain.Tests/Examples/ExampleDbContext.csfor a working reference
See the fully working examples in the test project:
Goodtocode.Domain.Tests/Examples/RowLevelSecurityExample.cs(row-level security, audit fields, and partition/row key usage)Goodtocode.Domain.Tests/Examples/CommandHandlerWithEventsExample.cs(command handlers, domain events, dispatcher, and service bus integration)Goodtocode.Domain.Tests/Examples/ExampleDbContext.cs(EF Core integration for audit and security fields)
| Version | Date | Release Notes |
|---|---|---|
| 1.0.0 | 2026-Jan-19 | Initial release |
| 1.2.0 | 2026-Mar-14 | Added rowKey support |
| 1.3.0 | 2026-Mar-18 | Versioning, pinning, freezing |
This project is licensed with the MIT license.