Roslyn Source Generator for .NET that automates Domain-Driven Design patterns and Clean Architecture components.
$ dotnet add package DomainSmith.AggregateRootRoslyn Source Generator for .NET that automates Domain-Driven Design patterns and Clean Architecture components.
DomainSmith is a set of Roslyn source generators that generate DDD/Clean Architecture boilerplate from simple domain types annotated with attributes:
ValueObject (immutability / value-based equality + factories and updates)Entity (ID + create/update API)AggregateRoot (ID + support for entity collections inside the aggregate)Repository (repository interface for an Aggregate Root)The generator does not replace your domain logic. Its goal is to generate a consistent API and extensions (for example Create(...), Update(...), entity-collection helpers, etc.) from the fields/properties you already defined.
Packages in src/ are independent and target netstandard2.0 (generators are consumed by application projects). Additionally, DomainSmith.Abstraction provides shared DDD primitives (ValueObject, Entity<TId>, AggregateRoot<TId>) and a lightweight Result pattern.
[ValueObject])Attribute: DomainSmith.ValueObject.ValueObjectAttribute
Generator input:
[ValueObject][ExcludeFromGeneration])Result pattern is enabledusing DomainSmith.Abstraction.Core.Result;
namespace DomainSmith.ValueObject.Examples.ValueObjects;
[ValueObject]
public partial record Money
{
public static int MaxAmount = 10000;
public static int MinAmount = 0;
public static int MaxCurrencyLength = 3;
public decimal Amount { get; init; }
public string Currency { get; init; } = "USD";
static partial void OnCreating(ref decimal amount, ref string currency, ref bool canCreate, ref Error error)
{
if (amount > MaxAmount)
{
canCreate = false;
error = new Error("Money.Amount.ExceedsMax", $"Amount cannot exceed {MaxAmount}.");
return;
}
if (amount < MinAmount)
{
canCreate = false;
error = new Error("Money.Amount.BelowMin", $"Amount cannot be below {MinAmount}.");
return;
}
if (currency.Length > MaxCurrencyLength)
{
canCreate = false;
error = new Error("Money.Currency.ExceedsMaxLength",
$"Currency cannot exceed {MaxCurrencyLength} characters.");
return;
}
}
}What gets generated:
// <auto-generated/>
using DomainSmith.Abstraction.Core.Result;
using DomainSmith.Abstraction.Core.Primitives;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using DomainSmith.Abstraction.Core.Result;
namespace DomainSmith.ValueObject.Examples.ValueObjects;
partial record Money
{
private Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public static Result<Money> Create(decimal amount, string currency)
{
bool canCreate = true;
Error error = Error.None;
OnCreating(ref amount, ref currency, ref canCreate, ref error);
var result = new Money(amount, currency);
OnCreated(result, ref canCreate, ref error);
if (!canCreate) return Result.Failure<Money>(error);
return Result.Success(result);
}
static partial void OnCreating(ref decimal amount, ref string currency, ref bool canCreate, ref Error error);
static partial void OnCreated(Money instance, ref bool canCreate, ref Error error);
}Create(...) and Update(...) (variants depend on Result pattern),Result pattern:
Result / Result<T>.[NoResultPattern], the generator switches to APIs without Result.[Entity(typeof(TId))])Attribute: DomainSmith.Entity.EntityAttribute
Generator input:
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Primitives;
using DomainSmith.Abstraction.Core.Result;
namespace DomainSmith.Entity.Examples.Entities;
[Entity(typeof(OwnerId))]
public partial class Owner
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
[ExcludeFromGeneration] public bool IsEnabled { get; private set; }
[AutoGenerated] public DateTime? ModifiedAt { get; private set; }
[AutoGenerated] public DateTime CreatedAt { get; private set; }
public Result Activate()
{
IsEnabled = true;
ModifiedAt = DateTime.Now;
return Result.Success();
}
public Result Deactivate()
{
IsEnabled = false;
ModifiedAt = DateTime.Now;
return Result.Success();
}
partial void CreateCreatedAt() => CreatedAt = DateTime.Now;
partial void UpdateCreatedAt() => ModifiedAt = DateTime.Now;
partial void UpdateModifiedAt() => ModifiedAt = DateTime.Now;
}
public record OwnerId(Guid Value) : EntityIdRecord<Guid>(Value);[Entity(typeof(SomeIdType))] – the generator reads the ID type from the attribute argument,record/class and the underlying value type) is used to tailor generation,[ExcludeFromGeneration]),Result pattern configuration.What gets generated:
// <auto-generated/>
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Primitives;
using DomainSmith.Abstraction.Core.Result;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using DomainSmith.Abstraction.Core.Result;
namespace DomainSmith.Entity.Examples.Entities;
partial class Owner : Entity<OwnerId>
{
private Owner(OwnerId id, string firstname, string lastname, string email) : base(id)
{
FirstName = firstname;
LastName = lastname;
Email = email;
}
internal static Result<Owner> Create(string firstname, string lastname, string email)
{
bool canCreate = true;
Error error = Error.None;
OnCreatingAuto(ref firstname, ref lastname, ref email, ref canCreate, ref error);
OnCreating(ref firstname, ref lastname, ref email, ref canCreate, ref error);
var result = new Owner(new OwnerId(Guid.NewGuid()), firstname, lastname, email);
OnCreated(result, ref canCreate, ref error);
if(!canCreate) return Result.Failure<Owner>(error);
result.CreateModifiedAt();
result.CreateCreatedAt();
return Result.Success(result);
}
internal Result<bool> Update(string firstname, string lastname, string email)
{
bool canUpdate = true;
Error error = Error.None;
OnUpdatingAuto(ref firstname, ref lastname, ref email, ref canUpdate, ref error);
OnUpdating(ref firstname, ref lastname, ref email, ref canUpdate, ref error);
var tmpFirstName = FirstName;
var tmpLastName = LastName;
var tmpEmail = Email;
FirstName = firstname;
LastName = lastname;
Email = email;
OnUpdated(ref canUpdate, ref error);
if(!canUpdate)
{
FirstName = tmpFirstName;
LastName = tmpLastName;
Email = tmpEmail;
return Result.Failure<bool>(error);
}
UpdateModifiedAt();
UpdateCreatedAt();
return Result.Success(true);
}
private static void OnCreatingAuto(ref string firstname, ref string lastname, ref string email, ref bool canCreate, ref Error error)
{
OnCreatingFirstName(ref firstname, ref canCreate, ref error);
OnCreatingLastName(ref lastname, ref canCreate, ref error);
OnCreatingEmail(ref email, ref canCreate, ref error);
}
private void OnUpdatingAuto(ref string firstname, ref string lastname, ref string email, ref bool canUpdate, ref Error error)
{
OnUpdatingFirstName(ref firstname, ref canUpdate, ref error);
OnUpdatingLastName(ref lastname, ref canUpdate, ref error);
OnUpdatingEmail(ref email, ref canUpdate, ref error);
}
static partial void OnCreating(ref string firstname, ref string lastname, ref string email, ref bool canCreate, ref Error error);
static partial void OnCreated(Owner result, ref bool canCreate, ref Error error);
partial void OnUpdating(ref string firstname, ref string lastname, ref string email, ref bool canUpdate, ref Error error);
partial void OnUpdated(ref bool canUpdate, ref Error error);
partial void CreateModifiedAt();
partial void CreateCreatedAt();
partial void UpdateModifiedAt();
partial void UpdateCreatedAt();
static partial void OnCreatingFirstName(ref string firstname, ref bool canCreate, ref Error error);
static partial void OnCreatingLastName(ref string lastname, ref bool canCreate, ref Error error);
static partial void OnCreatingEmail(ref string email, ref bool canCreate, ref Error error);
partial void OnUpdatingFirstName(ref string firstname, ref bool canUpdate, ref Error error);
partial void OnUpdatingLastName(ref string lastname, ref bool canUpdate, ref Error error);
partial void OnUpdatingEmail(ref string email, ref bool canUpdate, ref Error error);
}Create(...)) and modification (Update(...)),Entity<TId> (from DomainSmith.Abstraction),Result-based variants (default) or non-Result variants (with [NoResultPattern]).[AggregateRoot(typeof(TId))])Attribute: DomainSmith.AggregateRoot.AggregateRootAttribute
Generator input:
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Primitives;
using DomainSmith.Entity;
namespace DomainSmith.AggregateRoot.Examples.AggregateRoots;
public record OwnerId(Guid Value) : EntityIdRecord<Guid>(Value);
public record CarId(Guid Value) : EntityIdRecord<Guid>(Value);
[Entity(typeof(CarId))]
public partial class Car
{
public string Name { get; private set; }
public string Type { get; private set; }
public decimal Price { get; private set; }
}
[AggregateRoot(typeof(OwnerId))]
public partial class Owner
{
private readonly HashSet<Car> _newCars = [];
private readonly HashSet<Car> _oldCars = [];
[ExcludeFromGeneration] private readonly HashSet<Car> _allCars = [];
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
}[AggregateRoot(typeof(SomeIdType))] (similar to Entity),[ExcludeFromGeneration]),Result pattern configuration,What gets generated:
// <auto-generated/>
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Primitives;
using DomainSmith.Entity;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using DomainSmith.Abstraction.Core.Result;
using System.Linq;
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Result;
namespace DomainSmith.AggregateRoot.Examples.AggregateRoots;
partial class Owner : AggregateRoot<OwnerId>
{
private Owner(OwnerId id, string firstname, string lastname, string email) : base(id)
{
FirstName = firstname;
LastName = lastname;
Email = email;
}
public static Result<Owner> Create(string firstname, string lastname, string email)
{
bool canCreate = true;
Error error = Error.None;
OnCreatingAuto(ref firstname, ref lastname, ref email, ref canCreate, ref error);
OnCreating(ref firstname, ref lastname, ref email, ref canCreate, ref error);
var result = new Owner(new OwnerId(Guid.NewGuid()), firstname, lastname, email);
OnCreated(result, ref canCreate, ref error);
if(!canCreate) return Result.Failure<Owner>(error);
return Result.Success(result);
}
public Result<bool> Update(string firstname, string lastname, string email)
{
bool canUpdate = true;
Error error = Error.None;
OnUpdatingAuto(ref firstname, ref lastname, ref email, ref canUpdate, ref error);
OnUpdating(ref firstname, ref lastname, ref email, ref canUpdate, ref error);
var tmpFirstName = FirstName;
var tmpLastName = LastName;
var tmpEmail = Email;
FirstName = firstname;
LastName = lastname;
Email = email;
OnUpdated(ref canUpdate, ref error);
if(!canUpdate)
{
FirstName = tmpFirstName;
LastName = tmpLastName;
Email = tmpEmail;
return Result.Failure<bool>(error);
}
return Result.Success(true);
}
private static void OnCreatingAuto(ref string firstname, ref string lastname, ref string email, ref bool canCreate, ref Error error)
{
OnCreatingFirstName(ref firstname, ref canCreate, ref error);
OnCreatingLastName(ref lastname, ref canCreate, ref error);
OnCreatingEmail(ref email, ref canCreate, ref error);
}
private void OnUpdatingAuto(ref string firstname, ref string lastname, ref string email, ref bool canUpdate, ref Error error)
{
OnUpdatingFirstName(ref firstname, ref canUpdate, ref error);
OnUpdatingLastName(ref lastname, ref canUpdate, ref error);
OnUpdatingEmail(ref email, ref canUpdate, ref error);
}
static partial void OnCreating(ref string firstname, ref string lastname, ref string email, ref bool canCreate, ref Error error);
static partial void OnCreated(Owner result, ref bool canCreate, ref Error error);
partial void OnUpdating(ref string firstname, ref string lastname, ref string email, ref bool canUpdate, ref Error error);
partial void OnUpdated(ref bool canUpdate, ref Error error);
static partial void OnCreatingFirstName(ref string firstname, ref bool canCreate, ref Error error);
static partial void OnCreatingLastName(ref string lastname, ref bool canCreate, ref Error error);
static partial void OnCreatingEmail(ref string email, ref bool canCreate, ref Error error);
partial void OnUpdatingFirstName(ref string firstname, ref bool canUpdate, ref Error error);
partial void OnUpdatingLastName(ref string lastname, ref bool canUpdate, ref Error error);
partial void OnUpdatingEmail(ref string email, ref bool canUpdate, ref Error error);
public IReadOnlyCollection<Car> NewCars => _newCars;
public Result<Car> AddNewElementToNewCars(string name, string type, decimal price)
{
var result = Car.Create(name, type, price);
if (result.IsFailure)
return result;
var entity = result.Value();
_newCars.Add(entity);
return entity;
}
public Result UpdateElementInNewCars(CarId id, string name, string type, decimal price)
{
var entity = _newCars.FirstOrDefault(a => a.Id == id);
if (entity is null)
return Result.Failure(new Error("Owner.NewCars", "Not found"));
var result = entity.Update(name, type, price);
return result;
}
public Result DeleteElementFromNewCars(CarId id)
{
var entity = _newCars.FirstOrDefault(a => a.Id == id);
if (entity is null)
return Result.Failure(new Error("Owner.NewCars", "Not found"));
_newCars.Remove(entity);
return Result.Success();
}
public IReadOnlyCollection<Car> OldCars => _oldCars;
public Result<Car> AddNewElementToOldCars(string name, string type, decimal price)
{
var result = Car.Create(name, type, price);
if (result.IsFailure)
return result;
var entity = result.Value();
_oldCars.Add(entity);
return entity;
}
public Result UpdateElementInOldCars(CarId id, string name, string type, decimal price)
{
var entity = _oldCars.FirstOrDefault(a => a.Id == id);
if (entity is null)
return Result.Failure(new Error("Owner.OldCars", "Not found"));
var result = entity.Update(name, type, price);
return result;
}
public Result DeleteElementFromOldCars(CarId id)
{
var entity = _oldCars.FirstOrDefault(a => a.Id == id);
if (entity is null)
return Result.Failure(new Error("Owner.OldCars", "Not found"));
_oldCars.Remove(entity);
return Result.Success();
}
}Create(...)) and modification (Update(...)),AggregateRoot<TId> (from DomainSmith.Abstraction),Result-based variants (default) or non-Result variants (with [NoResultPattern]),If the generator detects that the aggregate has an entity collection (for example with a backing field), it generates:
IReadOnlyCollection<T> property exposing the collection,AddNewElementTo{CollectionName}(...)UpdateElementIn{CollectionName}(id, ...)DeleteElementFrom{CollectionName}(id)Result pattern variants:
Result pattern:
Add... returns Result<TElement>Update... / Delete... return ResultResult.Failure(new Error("{Aggregate}.{Collection}", "Not found"))Result pattern:
Add... returns TElement? (null on failure)Update... / Delete... are void (silent failure, e.g., element not found)Additionally, the generator detects whether the element entity itself uses the Result pattern (for example TElement.Create(...) may return Result<TElement>). In that case the generated code can:
TElement.Create(...)null) if creation/update fails.The repository generator is based on types annotated with [AggregateRoot].
Input:
using DomainSmith.AggregateRoot;
namespace DomainSmith.Repository.Examples.AggregateRoots;
[AggregateRoot(typeof(Guid))]
public partial class Owner
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
}[AggregateRoot(typeof(TId))]TIdWhat gets generated:
// <auto-generated/>
using DomainSmith.AggregateRoot;
using DomainSmith.Abstraction.Core.Primitives;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using DomainSmith.Abstraction.Core.Maybe;
namespace DomainSmith.Repository.Examples.AggregateRoots;
public interface IOwnerRepository
{
Task<DomainSmith.Abstraction.Core.Maybe.Maybe<Owner>> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<Owner>> GetAllAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(Owner owner, CancellationToken cancellationToken = default);
void Remove(Owner owner);
}I{AggregateRootName}Repository (the output filename is set explicitly in the generator),GetByIdAsync(...) to represent the possibility of a missing entity.partial and have stable properties that define the generated API.[ExcludeFromGeneration].Result pattern, add [assembly: NoResultPattern] or annotate individual types with [NoResultPattern].