DomainSmith.ValueObject 0.1.0

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DomainSmith.ValueObject" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DomainSmith.ValueObject" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="DomainSmith.ValueObject" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DomainSmith.ValueObject --version 0.1.0
                    
#r "nuget: DomainSmith.ValueObject, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DomainSmith.ValueObject@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DomainSmith.ValueObject&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=DomainSmith.ValueObject&version=0.1.0
                    
Install as a Cake Tool

DomainSmith

CICD codecov

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 pattern is 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(...) 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<T>.
  • if you use [NoResultPattern], the generator switches to APIs without Result.

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/class and the underlying value type) is used to tailor generation,
  • entity properties (excluding [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);
}
  • a consistent API for entity creation (Create(...)) and modification (Update(...)),
  • integration with Entity<TId> (from DomainSmith.Abstraction),
  • Result-based variants (default) or non-Result variants (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 pattern configuration,
  • 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> (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<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... returns Result<TElement>
    • 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<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 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].
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 90 1/26/2026
0.1.0 90 1/25/2026