An open API service indexing awesome lists of open source software.

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.

Awesome Lists containing this project

README

          

# DomainSmith

[![CICD](https://github.com/lukasz-strus/DomainSmith/actions/workflows/main.yml/badge.svg)](https://github.com/lukasz-strus/DomainSmith/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/lukasz-strus/DomainSmith/branch/main/graph/badge.svg?token=DAD5PSBP23)](https://codecov.io/gh/lukasz-strus/DomainSmith)
[![NuGet Badge](https://img.shields.io/nuget/v/DomainSmith.Abstraction.svg)](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]`.