https://github.com/lukasz-strus/domainsmith
Roslyn Source Generator for .NET that automates Domain-Driven Design patterns and Clean Architecture components.
https://github.com/lukasz-strus/domainsmith
clean-architecture code-generator csharp domain-driven-design dotnet dotnetstandart20 roslyn
Last synced: about 1 month ago
JSON representation
Roslyn Source Generator for .NET that automates Domain-Driven Design patterns and Clean Architecture components.
- Host: GitHub
- URL: https://github.com/lukasz-strus/domainsmith
- Owner: lukasz-strus
- License: mit
- Created: 2025-08-02T14:20:16.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2026-01-26T07:53:55.000Z (6 months ago)
- Last Synced: 2026-05-09T05:46:53.014Z (2 months ago)
- Topics: clean-architecture, code-generator, csharp, domain-driven-design, dotnet, dotnetstandart20, roslyn
- Language: C#
- Homepage:
- Size: 274 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# DomainSmith
[](https://github.com/lukasz-strus/DomainSmith/actions/workflows/main.yml)
[](https://codecov.io/gh/lukasz-strus/DomainSmith)
[](https://www.nuget.org/packages/DomainSmith.Abstraction/)
Roslyn Source Generator for .NET that automates Domain-Driven Design patterns and Clean Architecture components.
## General
`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`, `AggregateRoot`) and a lightweight `Result` pattern.
## Value Object (`[ValueObject]`)
Attribute: `DomainSmith.ValueObject.ValueObjectAttribute`
Generator input:
- a type (class/record) annotated with `[ValueObject]`
- properties on that type (excluding `[ExcludeFromGeneration]`)
- whether the `Result pattern` is enabled
```csharp
using 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:
```csharp
//
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 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(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);
}
```
- code that provides a consistent way to create and update the object based on defined properties,
- factory/update methods like `Create(...)` and `Update(...)` (variants depend on `Result pattern`),
- an extension/partial API for the type (the generator also tracks “extension name” and type reference).
`Result pattern`:
- by default, the generator produces APIs returning `Result` / `Result`.
- if you use `[NoResultPattern]`, the generator switches to APIs without `Result`.
## Entity (`[Entity(typeof(TId))]`)
Attribute: `DomainSmith.Entity.EntityAttribute`
Generator input:
```csharp
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(Value);
```
- a class annotated with `[Entity(typeof(SomeIdType))]` – the generator reads the ID type from the attribute argument,
- ID metadata (for example whether it is a `record`/`class` and the underlying value type) is used to tailor generation,
- entity properties (excluding `[ExcludeFromGeneration]`),
- `Result pattern` configuration.
What gets generated:
```csharp
//
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
{
private Owner(OwnerId id, string firstname, string lastname, string email) : base(id)
{
FirstName = firstname;
LastName = lastname;
Email = email;
}
internal static Result 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(error);
result.CreateModifiedAt();
result.CreateCreatedAt();
return Result.Success(result);
}
internal Result 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(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);
}
```
- a consistent API for entity creation (`Create(...)`) and modification (`Update(...)`),
- integration with `Entity` (from `DomainSmith.Abstraction`),
- `Result`-based variants (default) or non-`Result` variants (with `[NoResultPattern]`).
## Aggregate Root (`[AggregateRoot(typeof(TId))]`)
Attribute: `DomainSmith.AggregateRoot.AggregateRootAttribute`
Generator input:
```csharp
using DomainSmith.Abstraction.Common;
using DomainSmith.Abstraction.Core.Primitives;
using DomainSmith.Entity;
namespace DomainSmith.AggregateRoot.Examples.AggregateRoots;
public record OwnerId(Guid Value) : EntityIdRecord(Value);
public record CarId(Guid Value) : EntityIdRecord(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 _newCars = [];
private readonly HashSet _oldCars = [];
[ExcludeFromGeneration] private readonly HashSet _allCars = [];
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
}
```
- a class annotated with `[AggregateRoot(typeof(SomeIdType))]` (similar to Entity),
- aggregate properties (excluding `[ExcludeFromGeneration]`),
- `Result pattern` configuration,
- **EntityCollections** – the generator analyzes the class and detects entity collections for which it should generate collection-management APIs.
What gets generated:
```csharp
//
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
{
private Owner(OwnerId id, string firstname, string lastname, string email) : base(id)
{
FirstName = firstname;
LastName = lastname;
Email = email;
}
public static Result 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(error);
return Result.Success(result);
}
public Result 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(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 NewCars => _newCars;
public Result 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 OldCars => _oldCars;
public Result 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();
}
}
```
- a consistent API for aggregate creation (`Create(...)`) and modification (`Update(...)`),
- integration with `AggregateRoot` (from `DomainSmith.Abstraction`),
- `Result`-based variants (default) or non-`Result` variants (with `[NoResultPattern]`),
- helpers for managing entity collections inside the aggregate.
### EntityCollections in AggregateRoot
If the generator detects that the aggregate has an entity collection (for example with a backing field), it generates:
- optionally, an `IReadOnlyCollection` property exposing the collection,
- helper methods for collection management:
- `AddNewElementTo{CollectionName}(...)`
- `UpdateElementIn{CollectionName}(id, ...)`
- `DeleteElementFrom{CollectionName}(id)`
`Result pattern` variants:
- with `Result pattern`:
- `Add...` returns `Result`
- `Update...` / `Delete...` return `Result`
- when an element is not found, it returns `Result.Failure(new Error("{Aggregate}.{Collection}", "Not found"))`
- without `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`). In that case the generated code can:
- call `TElement.Create(...)`
- short-circuit and propagate the failure (or return `null`) if creation/update fails.
## Repository (for an Aggregate Root)
The repository generator is based on types annotated with `[AggregateRoot]`.
Input:
```csharp
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; }
}
```
- an Aggregate Root class annotated with `[AggregateRoot(typeof(TId))]`
- the ID type `TId`
What gets generated:
```csharp
//
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> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task> GetAllAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(Owner owner, CancellationToken cancellationToken = default);
void Remove(Owner owner);
}
```
- a repository interface named `I{AggregateRootName}Repository` (the output filename is set explicitly in the generator),
- method signatures tailored to the aggregate ID,
- Maybe-based `GetByIdAsync(...)` to represent the possibility of a missing entity.
## Practical notes
- For source generators, the target type should typically be `partial` and have stable properties that define the generated API.
- If you do not want a member to participate in generation (technical fields, caches, etc.), mark it with `[ExcludeFromGeneration]`.
- If you do not use the `Result pattern`, add `[assembly: NoResultPattern]` or annotate individual types with `[NoResultPattern]`.