DomainSmith.ValueObject
0.1.0
See the version list below for details.
dotnet add package DomainSmith.ValueObject --version 0.1.0
NuGet\Install-Package DomainSmith.ValueObject -Version 0.1.0
<PackageReference Include="DomainSmith.ValueObject" Version="0.1.0" />
<PackageVersion Include="DomainSmith.ValueObject" Version="0.1.0" />
<PackageReference Include="DomainSmith.ValueObject" />
paket add DomainSmith.ValueObject --version 0.1.0
#r "nuget: DomainSmith.ValueObject, 0.1.0"
#:package DomainSmith.ValueObject@0.1.0
#addin nuget:?package=DomainSmith.ValueObject&version=0.1.0
#tool nuget:?package=DomainSmith.ValueObject&version=0.1.0
DomainSmith
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<TId>, AggregateRoot<TId>) 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 patternis enabled
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:
// <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);
}
- code that provides a consistent way to create and update the object based on defined properties,
- factory/update methods like
Create(...)andUpdate(...)(variants depend onResult 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<T>. - if you use
[NoResultPattern], the generator switches to APIs withoutResult.
Entity ([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);
- 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/classand the underlying value type) is used to tailor generation, - entity properties (excluding
[ExcludeFromGeneration]), Result patternconfiguration.
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);
}
- a consistent API for entity creation (
Create(...)) and modification (Update(...)), - integration with
Entity<TId>(fromDomainSmith.Abstraction), Result-based variants (default) or non-Resultvariants (with[NoResultPattern]).
Aggregate Root ([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; }
}
- a class annotated with
[AggregateRoot(typeof(SomeIdType))](similar to Entity), - aggregate properties (excluding
[ExcludeFromGeneration]), Result patternconfiguration,- EntityCollections – the generator analyzes the class and detects entity collections for which it should generate collection-management APIs.
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();
}
}
- a consistent API for aggregate creation (
Create(...)) and modification (Update(...)), - integration with
AggregateRoot<TId>(fromDomainSmith.Abstraction), Result-based variants (default) or non-Resultvariants (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<T>property exposing the collection, - helper methods for collection management:
AddNewElementTo{CollectionName}(...)UpdateElementIn{CollectionName}(id, ...)DeleteElementFrom{CollectionName}(id)
Result pattern variants:
- with
Result pattern:Add...returnsResult<TElement>Update.../Delete...returnResult- when an element is not found, it returns
Result.Failure(new Error("{Aggregate}.{Collection}", "Not found"))
- without
Result pattern:Add...returnsTElement?(nullon failure)Update.../Delete...arevoid(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:
- 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:
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:
// <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);
}
- 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
partialand 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].
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- DomainSmith.Abstraction (>= 0.1.0)
- Microsoft.CodeAnalysis.Common (>= 5.0.0)
- Microsoft.CodeAnalysis.CSharp (>= 5.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.