CodeChops.DomainModeling
2.4.0
See the version list below for details.
dotnet add package CodeChops.DomainModeling --version 2.4.0
NuGet\Install-Package CodeChops.DomainModeling -Version 2.4.0
<PackageReference Include="CodeChops.DomainModeling" Version="2.4.0" />
paket add CodeChops.DomainModeling --version 2.4.0
#r "nuget: CodeChops.DomainModeling, 2.4.0"
// Install CodeChops.DomainModeling as a Cake Addin #addin nuget:?package=CodeChops.DomainModeling&version=2.4.0 // Install CodeChops.DomainModeling as a Cake Tool #tool nuget:?package=CodeChops.DomainModeling&version=2.4.0
Domain modeling (DDD)
This packages helps with modeling your code in accordance to the principles of Domain-Driven Design (DDD). It contains base types, factories, source generation, identities, validation, and helpers to provide you a clear way to implement DDD easily.
Check out CodeChops projects for more projects.
Base types
The base types include:
Value objects
A value object basically wraps an underlying value and exposes an API that is relevant for the value object. There are multiple characteristics of value objects that have to be taken into account when creating them: Value objects should be immutable, lightweight, have structural equality, and therefore shouldn't contain an ID. Value objects can be implemented correctly by using one of the following methods (ordered by preference):
- Use the value object generator by placing an attribute on a (record) struct/class. The parameters on the attribute will guide you to a correct implementation of the value object. The generator supports the following underlying types:
struct
,string
,list
, anddictionary
. See Value object generator for more information. - Implement the abstract
record
ValueObject<TSelf>
.GetHashCode
andEquals
do not have to be implemented. - Implement the abstract
class
ValueObjectClass<TSelf>
. Not recommended:GetHashCode
andEquals
now have to be implemented manually. Also, it is a good convention to use records for value objects, as records enforce structural equality out-of-the-box.
Entities
The abstract Entity
enforces a correct way to implement a reference type with an identity (ID). See also Identities.
Sometimes an entity contains a collection as underlying value. Therefore some commonly used collections are included in this package.
The following base-entities can be used if an entity uses a collection as underlying value:
- Dictionary entity
- HashSet entity
- List entity
These entities publish a readonly API (indices, ContainsKey, GetEnumerator, Count, ...). This way, less boilerplate has to be written manually.
Other types
- IDomainObject
- AggregateRoot
- IApplicationService
- IBoundedContext
- IDomainService
Validation
Validation should be performed in the constructor of an object. A Validator
class can be passed to constructors or factory methods Factories which gives control (from the outside) over the behaviour during (in)validation.
Validator
Provides a way to easily (in)validate domain objects. It support several modes to handle ValidationExceptions
:
Default
: throws an exception when the guard invalidates (immutable).DoNotThrow
: does not throw when the guard invalidates. This is being used by theTryCreate
factory, see Factories. It remembers every validation exception that occured during invalidation (mutable).Oblivious
: does not throw. It does not collect validation exception (immutable).
Guards
- Are an extension method on the
Validator
and contains logic to validate a domain object. It therefore guards that an object will be in a valid state. - Ensure that checks are centralized, can easily be re-used, and will throw consistent exception messages. Consistency of messages is especially relevant when using
Validation exceptions
, see Validation exceptions. - Help avoid the usage of the
throw
keyword in hot paths as throw keywords prevent JIT-inlining. - Accept an
ErrorCode
. This code is a unique code (in the scope of the domain) and can be used by other services to know which error has occured. When a code has been passed to the guard, a validation exception will be thrown. Ifnull
is provided, a system exception will be thrown.
Exceptions
ValidationExceptions
- Are represented in code as
ValidationException<TGuard>
. - Should occur after invalidation of external input.
- Contain an error code, a message, and values of the validation parameters, which can be communicated externally.
- This helps localization of messages shown to the end-user for external services. To consume and localize these messages, see the DDD Contracts library.
System exceptions
- Are represented in code as
SystemException<TGuard>
. - Contain system-information and therefore should not be visible to the user.
Factories
Factories can be created by implementing the static abstract method ICreatable<TObject,T1,T2,..)
(the value object generator will automatically create them for you). When they are implemented, a TryCreate
or Create
factory method can be used. The TryCreate
method offers a good way to try to construct domain objects without having to use an expensive try-catch
to catch the validation exception. This factory uses the DoNotThrow
Validator-method behind the scenes.
Examples
Create a value object that needs to be validated:
public enum ErrorCode
{
Name_IsNull,
Name_LengthOutOfRange,
Name_Invalid,
}
public record Name : ValueObject<Name>, ICreatable<Name, string>
{
public static Name Create(string name, Validator? validator = null)
=> new(name, validator);
private Name(string name, Validator? validator = null)
{
validator ??= Validator.Get<Name>.Default;
validator.GuardNotNull(name, errorCode: ErrorCode.Name_IsNull.ToString());
validator.GuardLengthInRange(name, 1, 15, errorCode: ErrorCode.Name_LengthOutOfRange.ToString());
validator.GuardRegex(name, "^[a-zA-Z][a-zA-Z0-9]*$", ErrorCode.Name_Invalid.ToString());
}
}
Behaviour:
- Name
null
will throw aValidationException<NotNullGuard<Name>>
with message "Required data 'value' for 'Name' is missing.". - Name 'ThisNameIsTooLong' will throw a
ValidationException<LengthInRangeGuard>
with message "Length is out of range for 'Name' 'ThisNameIsTooLong' (Lower bound: '1') (Upper bound: '15').'. - Name '1337' will throw a
ValidationException<RegexGuard>
with message "'Name' '1337' is incorrect.".
Now it's easy to perform TryCreate
on the object:
public record Person(Name Name) : ValueObject<Person>
{
public bool Exists(string nameString)
{
if (!ICreatable<Name, string>.TryCreate(nameString, out _))
return false;
/* etc */
return true;
}
}
Value object generator
A value object can easily be generated by placing an attribute on an readonly (ref) partial struct
. If this is not possible, place the attribute on a partial record/class
). The parameters on the attribute are settings which configure the creation of the value object.
The generator correctly implements a value object for you by:
- Forcing you to think about the ToString-implementation:<br/>You can implement
ToString
, or letting you choose the defaultToDisplayString
-method. This can be configured using attribute parametergenerateToString
. See also ToDisplayString. - Implementing
Equals
,GetHashCode
,IComparable
, and comparison operators:<br/>Comparison will only be implemented when the underlying value implementsIComparable
and the settinggenerateComparison
is enabled. - Generating a default constructor which supports
Validator
and thereforeGuards
:<br/> It even creates some standard validation, see Underlying types. This can be turned of by setting attribute parametergenerateDefaultConstructor
tofalse
. - Forces you to think about forbidding the usage of a parameterless constructor by default:<br/>
As structs automatically contain a parameterless constructor, something that can be forgotten easily. The generator will place an
Obsolete
attribute on a generated parameterless constructor if the required settingforbidParameterlessConstruction
is set totrue
. When the constructor is still being called, it will throw anInvalidOperationException
. - Generating a static property containing a default instance of the value object:<br/>
Value objects are immutable and therefore easily can provided a default value (if needed). This functionality can be turned on by setting
generateStaticDefault
totrue
. - Generating default casts for you:
- From the underlying value to the value object:
explicit cast
(because it could throw exceptions). - From the value object to the underlying value:
implicit cast
.
- From the underlying value to the value object:
- Letting you choose the property name which holds the underlying value:<br/>
This can be done by providing a value for parameter
propertyName
. If not provided, 'Value' will be used as property name. - Setting the accessibility of the property by default to
private
:<br/> This can be changed by settingpropertyIsPublic
totrue
. - Letting you think about if a
ValidationException
or aSystemException
should be thrown when aGuard
invalidates:<br/> Using the parameteruseValidationExceptions
. - Exposing indices and
IEnumerable
IReadOnly
-methods to value objects with an enumerable as underlying value:<br/> This can be turned off by setting parametergenerateEnumerable
tofalse
. - Adding the keyword
readonly
automatically for structs:<br/> This will ensure immutability and also optimize the struct. - Letting you think about if an underlying value should be
nullable
:<br/> Nullability can be enabled by settingvalueIsNullable
totrue
. - Adding the
StructLayoutAttribute
withLayoutKind.Auto
: <br/> It gives the CLR permission to reorder the bytes corresponding to these fields. It decides exactly how to reorganize the fields for memory usage, packing, etc. By default structs in C# are implemented withLayoutKind.Sequential
, because if types are commonly used for COM Interop, their fields must stay in the order they were defined. But because our ValueObjects are not expected to be used for COM Interop theLayoutKind.Auto
attribute is used to optimize the struct. - Adding basic validation for you:<br/> See Underlying types.
- Forcing you to think about string-equality:<br/>
Comparison behaviour should be provided for value objects that have an underlying value of
string
. - Generating
Length
orCount
properties:<br/> For string or enumerable value objects. - Automatically implementing
IValueObject
:<br/> Not implementing the interface can lead to unexpected and undesired behaviour in your code.
If the generated default constructor has to be extended or edited: copy the generated constructor, place it in your domain object, and edit it. Also set parameter
generateDefaultConstructor
tofalse
.
⚠️ Manually added properties won't be included in the structural equality.<br/> <br/> Manually added properties won't be used in the
Equals
,CompareTo
andGetHashCode
-calculation. To use value objects with multiple underlying values aValueTuple
can be provided as type parameter. The property should be set toprivate
and non-private expression bodied properties should be created that expose each value of the tuple. See the tuple example below.
Underlying types
The value object generator supports the following underlying types: struct
, string
, list
, and dictionary
. It can even generate objects with multiple underlying values in the form of ValueTuples
, see Tuple value object example.
Struct (default)
GenerateValueObjectAttribute<T>
<br/>
A value object with a struct
as underlying value, for example int
, DateTime
, decimal
. It has 2 optional settings minimumValue
and maximumValue
. When the underlying value implements IComparable
, the default constructor will guard that the value lies between these bounds.
Dictionary
GenerateDictionaryValueObjectAttribute<TKey, TValue>
<br/>
A value object with an immutable dictionary as underlying type. It has 2 optional settings: minimumCount
and maximumCount
. If provided, the default constructor will guard that the KeyValuePair
-count lies between these values.
List
GenerateListValueObjectAttribute<T>
<br/>
A value object with an immutable list as underlying type. It has 2 optional settings: minimumCount
, maximumCount
. If provided, the default constructor will guard that the element-count lies between these values.
String
GenerateStringValueObjectAttribute
<br/>
A value object that holds a string as underlying value. It has multiple settings:
minimumLength
andmaximumLength
(required). If provided, the default constructor will guard that the length of the string lies between these values.useRegex
(required). If true, it will force you to implement a static methodValidationRegex
which returns aRegex
. This regex will be used to (in)validate the object. See the example below.stringFormat
(required). The default constructor will guard that the string is of one of the following formats:Default
Alpha
AlphaWithUnderscore
AlphaNumeric
AlphaNumericWithUnderscore
stringComparison
(required). Configures the way strings should be compared, see StringComparison.stringCaseConversion
(optional). Configures if the string should be converted toLowerInvariant
orUpperInvariant
automatically. Default:NoConversion
.
Simple value object example
To create a simple PlayerAge
value object. Write the following code:
[GenerateValueObject<int>(
minimumValue: 0,
maximumValue: 120,
useValidationExceptions: false)]
public partial record struct PlayerAge;
It will generate the following code:
// <auto-generated />
#nullable enable
#pragma warning disable CS0612 // Is deprecated (level 1)
#pragma warning disable CS0618 // Member is obsolete (level 2)
using System;
using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using CodeChops.DomainModeling;
using CodeChops.DomainModeling.Exceptions;
using CodeChops.DomainModeling.Validation;
/// <summary>
/// An immutable value object with an underlying value of type Int32.
/// Extends: <see cref="global::PlayerAge"/>.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly partial record struct PlayerAge : IValueObject, ICreatable<PlayerAge, Int32>, IEquatable<PlayerAge>, IComparable<PlayerAge>
{
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override string ToString() => this.ToDisplayString(new { this.Value });
#region Property
/// <summary>
/// Get the underlying structural value.
/// </summary>
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
private Int32 Value => this._value1332;
/// <summary>
/// Backing field for <see cref='Value'/>. Don't use this field, use the Value property instead.
/// </summary>
[Obsolete("Don't use this field, use the Value property instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
private readonly Int32 _value1332 = default(Int32);
#endregion
#region Equals
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => this.Value.GetHashCode();
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Equals(PlayerAge other) => this.Value.Equals(other.Value);
#endregion
#region Comparison
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public int CompareTo(PlayerAge other) => this.Value.CompareTo(other.Value);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator < (PlayerAge left, PlayerAge right) => left.CompareTo(right) < 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator <= (PlayerAge left, PlayerAge right) => left.CompareTo(right) <= 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator > (PlayerAge left, PlayerAge right) => left.CompareTo(right) > 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator >= (PlayerAge left, PlayerAge right) => left.CompareTo(right) >= 0;
#endregion
#region Casts
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator Int32(PlayerAge value) => value.Value;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static explicit operator PlayerAge(Int32 value) => new(value);
#endregion
#region Constructors
[DebuggerHidden]
public PlayerAge(Int32 value, Validator? validator = null)
{
validator ??= Validator.Get<PlayerAge>.Default;
validator.GuardInRange<Int32>((Int32)value, 0, 120, errorCode: null);
this._value1332 = value;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
[Obsolete("Don't use this empty constructor. A Int32 should be provided when initializing PlayerAge.", true)]
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public PlayerAge() => throw new InvalidOperationException($"Don't use this empty constructor. A Int32 should be provided when initializing PlayerAge.");
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor
#endregion
#region Factories
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate(Int32 value, out PlayerAge createdObject)
=> ICreatable<PlayerAge, Int32>.TryCreate(value, out createdObject, out _);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate(Int32 value, out PlayerAge createdObject, out Validator validator)
=> ICreatable<PlayerAge, Int32>.TryCreate(value, out createdObject, out validator);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static PlayerAge Create(Int32 value, Validator? validator = null)
=> new(value, validator);
#endregion
}
#pragma warning restore CS0618 // Member is obsolete (level 2)
#pragma warning restore CS0612 // Is deprecated (level 1)
#nullable restore
String value object example
The Uuid
provided in this package is created using dogfooding principles. It is implemented as follows:
/// <summary>
/// A 32-digit UUID without hyphens.
/// </summary>
[GenerateStringValueObject(
minimumLength: 32,
maximumLength: 36,
useRegex: true,
stringFormat: StringFormat.Default,
stringComparison: StringComparison.Ordinal,
forbidParameterlessConstruction: false,
useValidationExceptions: false)]
public partial record struct Uuid
{
[GeneratedRegex("^[0-9A-F]{32}$", RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 1000)]
public static partial Regex ValidationRegex();
public Uuid()
: this(Guid.NewGuid().ToString("N").ToUpper())
{
}
public Uuid(Guid uuid)
: this(uuid.ToString("N").ToUpper())
{
}
}
This results in the following generated code:
// <auto-generated />
#nullable enable
#pragma warning disable CS0612 // Is deprecated (level 1)
#pragma warning disable CS0618 // Member is obsolete (level 2)
using System;
using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using CodeChops.DomainModeling;
using CodeChops.DomainModeling.Exceptions;
using CodeChops.DomainModeling.Validation;
namespace CodeChops.DomainModeling.Identities;
/// <summary>
/// An immutable value type with a Default-Formatted string as underlying value.
/// Extends: <see cref="global::CodeChops.DomainModeling.Identities.Uuid"/>.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly partial record struct Uuid : IValueObject, ICreatable<Uuid, String>, IEquatable<Uuid>, IComparable<Uuid>, IEnumerable<Char>, IHasValidationRegex
{
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override string ToString() => this.ToDisplayString(new { this.Value });
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public int Length => this.Value.Length;
#region Property
/// <summary>
/// Get the underlying structural value.
/// </summary>
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
private String Value => this._value6796 ?? String.Empty;
/// <summary>
/// Backing field for <see cref='Value'/>. Don't use this field, use the Value property instead.
/// </summary>
[Obsolete("Don't use this field, use the Value property instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
private readonly String _value6796 = String.Empty;
#endregion
#region Equals
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => String.GetHashCode(this.Value, StringComparison.Ordinal);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Equals(Uuid other) => String.Equals(this.Value, other.Value, StringComparison.Ordinal);
#endregion
#region Comparison
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public int CompareTo(Uuid other) => String.Compare(this.Value, other.Value, StringComparison.Ordinal);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator < (Uuid left, Uuid right) => left.CompareTo(right) < 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator <= (Uuid left, Uuid right) => left.CompareTo(right) <= 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator > (Uuid left, Uuid right) => left.CompareTo(right) > 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator >= (Uuid left, Uuid right) => left.CompareTo(right) >= 0;
#endregion
#region Casts
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator String(Uuid value) => value.Value;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static explicit operator Uuid(String value) => new(value);
#endregion
#region Enumerator
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public IEnumerator<Char> GetEnumerator() => this.Value.GetEnumerator();
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
#endregion
#region Constructors
[DebuggerHidden]
public Uuid(String value, Validator? validator = null)
{
validator ??= Validator.Get<Uuid>.Default;
validator.GuardNotNull(value, errorCode: null);
validator.GuardLengthInRange(value, 32, 36, errorCode: null);
validator.GuardRegex(value, ValidationRegex(), errorCode: null);
this._value6796 = value;
}
#endregion
#region Factories
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate(String value, out Uuid createdObject)
=> ICreatable<Uuid, String>.TryCreate(value, out createdObject, out _);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate(String value, out Uuid createdObject, out Validator validator)
=> ICreatable<Uuid, String>.TryCreate(value, out createdObject, out validator);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static Uuid Create(String value, Validator? validator = null)
=> new(value, validator);
#endregion
#region TypeSpecific
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public Char? this[int index]
=> Validator.Get<Uuid>.Default.GuardIndexInRange(this.Value, index, errorCode: null)!;
#endregion
}
#pragma warning restore CS0618 // Member is obsolete (level 2)
#pragma warning restore CS0612 // Is deprecated (level 1)
#nullable restore
Tuple value object example
The ValidationExceptionMessage
used in this package is also created using dogfooding principles. It is implemented as follows:
namespace CodeChops.DomainModeling.Exceptions.Validation;
/// <summary>
/// A validation message is communicated externally and contains a string message and parameters (which can be used for String.Format).
/// </summary>
[GenerateValueObject<(string, ImmutableList<object>)>(minimumValue: 0, maximumValue: Int32.MaxValue, generateToString: false, useValidationExceptions: false)]
public readonly partial record struct ValidationExceptionMessage
{
public override partial string ToString() => String.Format(this.Message, this.Parameters.ToArray());
/// <summary>
/// Is communicated externally!
/// </summary>
public string Message => this.Value.Item1;
/// <summary>
/// Is communicated externally!
/// </summary>
public ImmutableList<object?> Parameters => this.Value.Item2!;
public ValidationExceptionMessage(string objectName, string message, IEnumerable<object?> parameters)
: this((message, new object?[] { objectName}.Concat(parameters).ToImmutableList())!)
{
}
public ValidationExceptionMessage(string objectName, string message, object? parameter)
: this((message, new[] { objectName, parameter }.ToImmutableList())!)
{
}
/// <summary>
/// Used for deserialization.
/// </summary>
internal ValidationExceptionMessage(string message, IEnumerable<object?> parameters)
: this((message, parameters.ToImmutableList())!)
{
}
}
This generates the following code:
// <auto-generated />
#nullable enable
#pragma warning disable CS0612 // Is deprecated (level 1)
#pragma warning disable CS0618 // Member is obsolete (level 2)
using System;
using System.Collections;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using CodeChops.DomainModeling;
using CodeChops.DomainModeling.Exceptions;
using CodeChops.DomainModeling.Validation;
namespace CodeChops.DomainModeling.Exceptions.Validation;
/// <summary>
/// An immutable value object with an underlying value of type (String, ImmutableList<Object>).
/// Extends: <see cref="global::CodeChops.DomainModeling.Exceptions.Validation.ValidationExceptionMessage"/>.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly partial record struct ValidationExceptionMessage : IValueObject, ICreatable<ValidationExceptionMessage, (String, ImmutableList<Object>)>, IEquatable<ValidationExceptionMessage>, IComparable<ValidationExceptionMessage>
{
public override partial string ToString();
#region Property
/// <summary>
/// Get the underlying structural value.
/// </summary>
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
private (String, ImmutableList<Object>) Value => this._value2512;
/// <summary>
/// Backing field for <see cref='Value'/>. Don't use this field, use the Value property instead.
/// </summary>
[Obsolete("Don't use this field, use the Value property instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
private readonly (String, ImmutableList<Object>) _value2512 = default((String, ImmutableList<Object>));
#endregion
#region Equals
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => this.Value.GetHashCode();
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Equals(ValidationExceptionMessage other) => this.Value.Equals(other.Value);
#endregion
#region Comparison
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public int CompareTo(ValidationExceptionMessage other) => this.Value.CompareTo(other.Value);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator < (ValidationExceptionMessage left, ValidationExceptionMessage right) => left.CompareTo(right) < 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator <= (ValidationExceptionMessage left, ValidationExceptionMessage right) => left.CompareTo(right) <= 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator > (ValidationExceptionMessage left, ValidationExceptionMessage right) => left.CompareTo(right) > 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator >= (ValidationExceptionMessage left, ValidationExceptionMessage right) => left.CompareTo(right) >= 0;
#endregion
#region Casts
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator (String, ImmutableList<Object>)(ValidationExceptionMessage value) => value.Value;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static explicit operator ValidationExceptionMessage((String, ImmutableList<Object>) value) => new(value);
#endregion
#region Constructors
[DebuggerHidden]
public ValidationExceptionMessage((String, ImmutableList<Object>) value, Validator? validator = null)
{
validator ??= Validator.Get<ValidationExceptionMessage>.Default;
this._value2512 = value;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
[Obsolete("Don't use this empty constructor. A (String, ImmutableList<Object>) should be provided when initializing ValidationExceptionMessage.", true)]
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public ValidationExceptionMessage() => throw new InvalidOperationException($"Don't use this empty constructor. A (String, ImmutableList<Object>) should be provided when initializing ValidationExceptionMessage.");
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor
#endregion
#region Factories
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate((String, ImmutableList<Object>) value, out ValidationExceptionMessage createdObject)
=> ICreatable<ValidationExceptionMessage, (String, ImmutableList<Object>)>.TryCreate(value, out createdObject, out _);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool TryCreate((String, ImmutableList<Object>) value, out ValidationExceptionMessage createdObject, out Validator validator)
=> ICreatable<ValidationExceptionMessage, (String, ImmutableList<Object>)>.TryCreate(value, out createdObject, out validator);
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValidationExceptionMessage Create((String, ImmutableList<Object>) value, Validator? validator = null)
=> new(value, validator);
#endregion
}
#pragma warning restore CS0618 // Member is obsolete (level 2)
#pragma warning restore CS0612 // Is deprecated (level 1)
#nullable restore
Generic type example
Unfortunately, attribute type arguments cannot use type parameters. In case you want to create a open generic default value object, just add a type parameter to the definition of the type and the type provided in the attribute will be overwritten.
/* Default value object */
// Because no type is provided, the first generic type parameter (TNumber) will be used as underlying value.
[GenerateValueObject(underlyingType: null, useValidationExceptions: false)]
public partial record struct Point<TNumber>
where TNumber : struct, INumber<TNumber>;
/* List value object */
// Because no type is provided, the first generic type parameter (TId) will be used as element value.
[GenerateListValueObject(elementType: null, useValidationExceptions: false)]
public partial record struct IdList<TId>
where TId : IId<TId>, IEquatable<TId>, IComparable<TId>;
/* Dictionary value object */
// Because no value type is provided, the first generic type parameter (TObject) will be used for the value of the dictionary.
[GenerateDictionaryValueObject(keyType: typeof(string), valueType: null, useValidationExceptions: false)]
public partial record struct ObjectsByKey<TObject>
where TObject : IDomainObject;
// Because no key and value type is provided, TKey will be used for the key of the dictionary, and TValue for the value.
[GenerateDictionaryValueObject(keyType: null, valueType: null, useValidationExceptions: false)]
public partial record struct ObjectsByKey<TKey, TObject>
where TKey : IId
where TObject : IDomainObject;
⚠️ As you can see in the example above, non-generic attributes can aso be used to generate value object. Use these non-generic attributes for Blazor WebAssembly, because at the moment generic attributes don't work there: https://github.com/dotnet/runtime/issues/77047.
Identities
Entities require a unique identity (ID) in order to be distinguished from other entities. Each entity should have it's strongly typed ID.
It's important that the scope in which the ID should unique is taken into consideration before implementing them.
The underlying value of an identity should implement IEquatable
and IComparable
. It can be one of the following types:
- An underlying type (like
string
,ulong
,int
,byte
, etc.). - A (custom implemented)
ValueObject
. - A
Guid
. - A
Uuid
(which is included in this package, see Simple value object example): a 32-digit UUID without hyphens. - Any other
struct
which implementsIEquatable
andIComparable
.
Identities can be implemented correctly by using one of the following methods (ordered by preference):
- Use the Identity generator. This way you can create a readonly struct (which probably lives on the stack).
- Implementing the abstract record
IdBase
. Not recommended as this creates a reference type. - Implementing IId. Not recommended as
Equals
andGetHashCode
should manually correctly be implemented.
A SingletonId<TObject>
is also supported. This is convenient for implementing IDs of objects that only have one ID per type.
To enable correct JSON serialization of identities, add the following line to your Program.cs
builder.Services.AddIdentityJsonSerialization();
Identity generator
An identity belongs to an entity. To create an identity for an entity, simply make the class partial
and add the attribute GenerateIdentity<T>
(where T
is the underlying value).
The type parameter <T
> can also be omitted. In this case the underlying value of the identity will be ulong
.
A readonly partial struct
will be generated which is nested in the entity class. The entity will also get a property which contains an instance of the identity.
Two parameters can be provided when adding the attribute:
name
. If provided, this will be the name of the identity. The default name isIdentity
.propertyName
. The name of the identity-property of the entity class. The default name isId
.
If entity
Player
has aGenerateIdentity
-attribute (with default arguments):
Player.Identity
references the identity type.Player.Id
references the instance of the identity.
Entity with identity example
The following example will generate a strongly typed identity for entity Player
:
[GenerateIdentity<Uuid]
public partial class Player
{
}
The following code will be generated:
// <auto-generated />
#nullable enable
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using CodeChops.DomainModeling.Identities;
public partial class Player : IHasId
{
public IId Id { get; } = new Identity();
public readonly partial record struct Identity : IId<Identity, global::CodeChops.DomainModeling.Identities.Uuid>
{
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public override string ToString() => this.ToDisplayString(new { this.Value, UnderlyingType = nameof(global::CodeChops.DomainModeling.Identities.Uuid) });
[EditorBrowsable(EditorBrowsableState.Never)]
public global::CodeChops.DomainModeling.Identities.Uuid Value { get; private init; }
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static explicit operator Identity(global::CodeChops.DomainModeling.Identities.Uuid value) => new() { Value = value };
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static implicit operator global::CodeChops.DomainModeling.Identities.Uuid(Identity id) => id.Value;
#region Comparison
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public int CompareTo(Identity other)
=> this.Value.CompareTo((Identity)other.GetValue());
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator < (Identity left, Identity right) => left.CompareTo(right) < 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator <= (Identity left, Identity right) => left.CompareTo(right) <= 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator > (Identity left, Identity right) => left.CompareTo(right) > 0;
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator >= (Identity left, Identity right) => left.CompareTo(right) >= 0;
#endregion
/// <summary>
/// Warning. Probably performs boxing!
/// </summary>
[DebuggerHidden]
public object GetValue() => this.Value;
[DebuggerHidden]
public bool HasDefaultValue => this.Value.Equals(IId<global::CodeChops.DomainModeling.Identities.Uuid>.DefaultValue);
[DebuggerHidden]
public Identity(global::CodeChops.DomainModeling.Identities.Uuid value)
{
this.Value = value;
}
[DebuggerHidden]
public Identity()
{
this.Value = default(global::CodeChops.DomainModeling.Identities.Uuid);
}
}
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override int GetHashCode()
{
return this.Id.HasDefaultValue
? HashCode.Combine(this)
: this.Id.GetHashCode();
}
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool Equals(Player? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (other.GetType() != this.GetType()) return false;
return !this.Id.HasDefaultValue && this.Id.Equals(other.Id);
}
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override bool Equals(object? obj)
{
return obj is Player other
&& obj.GetType() == this.GetType()
&& this.Equals(other);
}
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator ==(Player? left, Player? right)
{
if (left is null && right is null) return true;
if (left is null || right is null) return false;
return left.Equals(right);
}
[DebuggerHidden]
[EditorBrowsable(EditorBrowsableState.Never)]
public static bool operator !=(Player? left, Player? right)
=> !(left == right);
}
#nullable restore
Miscellaneous
ToDisplayString
An extension method on IDomainObject
which can be used when overriding ToString
.
It creates a display string of the domain object by serializing it and changing the ':' to '='.
The value returned can be configured by providing an anonymous object to customize the string. If omitted, it will create a string of all fields and properties of the class/struct.
For example, to only show the ID of an entity, use:
public string override ToString() => this.ToDisplayString(new { Id = this.Id });
Tuple iterator
Iterates each value of a ValueTuple
.
Range iterator
A custom easy-to-use number iterator which uses the Ranges
-feature (introduced in C# 8).
foreach (var number in [1..4])
Console.WriteLine(number);
Will write:
1
2
3
ValueTuple JSON converter
To enable correct JSON serialization of value tuples, add the following line to Program.cs
builder.Services.AddValueTupleJsonSerialization();
Global using generator
This packages automatically generates global usings
for namespaces in this package. Namespaces in this file are often referenced when domain modeling and do not have to be manually referenced anymore.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. 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. |
-
net7.0
NuGet packages (7)
Showing the top 5 NuGet packages that depend on CodeChops.DomainModeling:
Package | Downloads |
---|---|
CodeChops.MagicEnums
Fast, customizable, and extendable enums for C# with a clean API. |
|
CodeChops.ImplementationDiscovery
Provides easy-accessible, design-time and runtime information about implementations throughout your code. |
|
CodeChops.Geometry
Contains objects and helpers to help the calculation of objects in 2D-space and time. |
|
CodeChops.LightResources
Light and dynamic resources for your Blazor WebAssembly website. |
|
CodeChops.Contracts
Easy use of contracts, adapters and polymorphism, using JSON. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.18.4 | 107 | 11/18/2024 |
2.18.3 | 56 | 11/18/2024 |
2.18.2 | 193 | 10/26/2024 |
2.18.1 | 68 | 10/26/2024 |
2.18.0 | 105 | 10/21/2024 |
2.17.8 | 526 | 9/29/2024 |
2.17.7 | 207 | 9/26/2024 |
2.17.6 | 90 | 9/26/2024 |
2.17.5 | 89 | 9/26/2024 |
2.17.4 | 89 | 9/26/2024 |
2.17.3 | 88 | 9/26/2024 |
2.17.2 | 762 | 2/28/2024 |
2.17.1 | 245 | 2/28/2024 |
2.17.0 | 190 | 2/28/2024 |
2.16.0 | 925 | 3/23/2023 |
2.15.0 | 932 | 3/20/2023 |
2.14.0 | 756 | 3/20/2023 |
2.13.0 | 761 | 3/17/2023 |
2.12.3 | 780 | 3/12/2023 |
2.12.1 | 765 | 3/12/2023 |
2.12.0 | 727 | 3/12/2023 |
2.11.1 | 721 | 3/12/2023 |
2.11.0 | 735 | 3/12/2023 |
2.10.5 | 865 | 3/10/2023 |
2.10.4 | 788 | 3/9/2023 |
2.10.3 | 746 | 3/6/2023 |
2.10.1 | 728 | 3/6/2023 |
2.10.0 | 906 | 3/5/2023 |
2.9.1 | 786 | 3/3/2023 |
2.8.0 | 941 | 1/27/2023 |
2.7.0 | 828 | 1/22/2023 |
2.6.1 | 855 | 1/22/2023 |
2.6.0 | 928 | 1/22/2023 |
2.4.2 | 887 | 1/21/2023 |
2.4.0 | 946 | 1/21/2023 |
2.3.4 | 821 | 1/20/2023 |
2.3.3 | 832 | 1/20/2023 |
2.3.2 | 815 | 1/19/2023 |
2.3.0 | 1,022 | 1/16/2023 |
2.2.0 | 848 | 1/15/2023 |
2.1.6 | 801 | 1/15/2023 |
2.1.5 | 808 | 1/15/2023 |
2.1.4 | 824 | 1/15/2023 |
2.1.3 | 822 | 1/15/2023 |
2.1.2 | 811 | 1/15/2023 |
2.0.1 | 1,543 | 1/6/2023 |
2.0.0 | 834 | 1/6/2023 |
Fixed null reference during value object generation when parameter substitution.