Unpacking the BeyondNet.Ddd ValueObject.cs
Value objects represent immutable, thread-safe, and self-contained data structures that capture business logic and invariants. By leveraging the ValueObject
class, developers can create custom value objects that ensure data consistency and integrity. This abstraction enables a more expressive and robust domain model, aligning with DDD principles.
using BeyondNet.Ddd.Interfaces;
using BeyondNet.Ddd.Rules;
using BeyondNet.Ddd.Rules.Impl;
using BeyondNet.Ddd.Rules.PropertyChange;
using BeyondNet.Ddd.Services.Impl;
namespace BeyondNet.Ddd
{
/// <summary>
/// Base class for value objects in the domain-driven design.
/// </summary>
/// <typeparam name="TValue">The type of the value object.</typeparam>
public abstract class ValueObject<TValue> : AbstractNotifyPropertyChanged, IProps
{
#region Members
public TrackingManager Tracking { get; private set; }
private ValidatorRuleManager<AbstractRuleValidator<ValueObject<TValue>>> _validatorRules = new();
private BrokenRulesManager _brokenRules = new BrokenRulesManager();
public bool IsValid => !_brokenRules.GetBrokenRules().Any();
#endregion
#region Properties
/// <summary>
/// Gets or sets the value of the value object.
/// </summary>
protected TValue Value
{
get
{
return (TValue)GetValue();
}
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ValueObject{TValue}"/> class.
/// </summary>
/// <param name="value">The value of the value object.</param>
protected ValueObject(TValue value)
{
ArgumentNullException.ThrowIfNull(value, nameof(value));
RegisterProperty(nameof(Value), typeof(TValue), value, ValuePropertyChanged);
Tracking = TrackingManager.MarkNew();
#pragma warning disable CA2214 // Do not call overridable methods in constructors
AddValidators();
#pragma warning restore CA2214 // Do not call overridable methods in constructors
Validate();
}
#endregion
#region Methods
private void ValuePropertyChanged(AbstractNotifyPropertyChanged sender, NotifyPropertyChangedContextArgs e)
{
Tracking = TrackingManager.MarkDirty();
Validate();
}
public void SetValue(TValue value)
{
ArgumentNullException.ThrowIfNull(value, nameof(value));
SetValue(value, nameof(Value));
}
public TValue GetValue()
{
return (TValue)GetValue(nameof(Value));
}
#endregion
#region Business Rules
/// <summary>
/// Adds the validators for the value object.
/// </summary>
public virtual void AddValidators()
{
}
private void Validate()
{
_brokenRules.Clear();
_brokenRules.Add(_validatorRules.GetBrokenRules());
}
public void AddValidator(AbstractRuleValidator<ValueObject<TValue>> validator)
{
_validatorRules.Add(validator);
}
public void AddValidators(ICollection<AbstractRuleValidator<ValueObject<TValue>>> validators)
{
_validatorRules.Add(validators);
}
public void RemoveValidator(AbstractRuleValidator<ValueObject<TValue>> validator)
{
_validatorRules.Remove(validator);
}
public IReadOnlyCollection<BrokenRule> GetBrokenRules()
{
return this._brokenRules.GetBrokenRules();
}
#endregion
#region Equality
protected static bool EqualOperator(ValueObject<TValue> left, ValueObject<TValue> right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, null) || left.Equals(right!);
}
protected static bool NotEqualOperator(ValueObject<TValue> left, ValueObject<TValue> right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
/// <summary>
/// Determines whether the current value object is equal to another value object.
/// </summary>
/// <param name="obj">The value object to compare with the current value object.</param>
/// <returns><c>true</c> if the current value object is equal to the other value object; otherwise, <c>false</c>.</returns>
public override bool Equals(object? obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject<TValue>)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
/// <summary>
/// Gets the hash code of the value object.
/// </summary>
/// <returns>The hash code of the value object.</returns>
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
/// <summary>
/// Creates a copy of the value object.
/// </summary>
/// <returns>A copy of the value object.</returns>
public ValueObject<TValue> GetCopy()
{
return (ValueObject<TValue>)MemberwiseClone();
}
public object Clone()
{
return GetCopy();
}
#endregion
}
}
Let's go through the code and understand its different sections:
Namespace and Class Declaration:
The code is placed inside the BeyondNet.Ddd namespace.
The class is declared as abstract and is named ValueObject.
The class inherits from AbstractNotifyPropertyChanged and implements the IProps interface.
The class is generic, with the generic type parameter TValue representing the type of the value object.
Members:
The class has a public property called Tracking of type TrackingManager. This property is used to track changes to the value object.
The class has a private field validatorRules of type ValidatorRuleManager<AbstractRuleValidator<ValueObject>>*. This field is used to manage the validators associated with the value object.*
The class has a private field brokenRules of type BrokenRulesManager. This field is used to manage the broken rules associated with the value object.
The class has a public read-only property IsValid of type bool. This property returns true if there are no broken rules, indicating that the value object is valid.
Properties:
- The class has a protected property Value of type TValue. This property is used to get the value of the value object.
Constructors:
The class has a constructor that takes a parameter value of type TValue. It initializes the value object with the provided value.
Inside the constructor, it performs the following tasks:
Throws an ArgumentNullException if the value parameter is null.
Registers the Value property for property change notifications using the RegisterProperty method.
Marks the Tracking property as new using the TrackingManager.MarkNew method.
Calls the AddValidators method to add any validators associated with the value object.
Calls the Validate method to validate the value object.
Methods:
The class has a private method ValuePropertyChanged that is called when the Value property changes. It marks the Tracking property as dirty and calls the Validate method.
The class has public methods SetValue and GetValue to set and get the value of the value object, respectively.
Business Rules:
The class has a virtual method AddValidators that can be overridden to add validators for the value object. By default, this method does nothing.
The class has a private method Validate that clears the broken rules and adds the broken rules returned by the validatorRules to the brokenRules manager.
The class has public methods AddValidator, AddValidators, and RemoveValidator to add, add a collection of, and remove validators from the validatorRules manager.
The class has a public method GetBrokenRules that returns the broken rules from the brokenRules manager.
Equality:
The class has protected static methods EqualOperator and NotEqualOperator that are used to compare two value objects for equality.
The class has an abstract method GetEqualityComponents that must be implemented by derived classes to provide the equality components of the value object.
The class overrides the Equals, GetHashCode, and Clone methods to provide equality comparison, hash code generation, and cloning functionality for the value object, respectively. Overall, this base class provides a foundation for creating value objects in domain-driven design. It includes features for tracking changes, validating the value object, managing broken rules, and implementing equality comparison. Derived classes can override methods and add validators specific to their value objects.
What is the purpose of the AbstractNotifyPropertyChanged class?
The AbstractNotifyPropertyChanged class serves as a base class that implements the INotifyPropertyChanged interface, which is commonly used in .NET to notify clients, typically binding clients, that a property value has changed. This is particularly useful in scenarios such as data binding in WPF or other UI frameworks. Here are the key purposes and functionalities of the AbstractNotifyPropertyChanged class:
Property Change Notification: It provides mechanisms to notify subscribers when a property value changes. This is done through the PropertyChanged event defined in the INotifyPropertyChanged interface.
Property Registration: The class allows properties to be registered using the RegisterProperty method. This method can also associate a callback (NotifyPropertyChangedHandler) that gets invoked when the property changes.
Value Management: It provides methods to get and set property values (GetValue, SetValue, ForceSetValue). These methods ensure that property change notifications are properly handled.
Callback Management: The class allows registering and unregistering property change callbacks using RegisterPropertyChangedCallback and UnregisterPropertyChangedCallback.
Event Invocation Control: It includes properties like IsCallbackInvokingEnabled and IsEventInvokingEnabled to control whether callbacks and events should be invoked when a property changes.
Validation: It includes internal mechanisms to validate property values against their types.
Creating a Value Object: Email
The selected code represents a class called Email which is a value object in the domain of an application. The Email class inherits from a base class called ValueObject. In this case, the generic type parameter string indicates that the value of the Email object is of a type string. The Email class has a constructor that takes a string parameter called value. This constructor calls the base class constructor ValueObject(value) to initialize the value of the Email object. The Email class overrides the GetEqualityComponents method, which is a protected method defined in the base class. This method is used to determine the equality of two Email objects. In this case, the GetEqualityComponents method returns a single object, which is the Value property of the Email object. This means that two Email objects are considered equal if their Value properties are equal.
public class Email : ValueObject<string>
{
public Email(string value) : base(value)
{
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public override void AddValidators()
{
base.AddValidators();
AddValidator(new EmailValidator(this));
}
}
Adding a Validator Rule
The selected code represents a class called EmailValidator that is responsible for validating email addresses. It is a subclass of the AbstractRuleValidator class, which is a generic class that takes a ValueObject as its type parameter. The EmailValidator class has a constructor that takes a ValueObject object called subject as a parameter. It calls the base constructor of AbstractRuleValidator and passes the subject parameter to it. The EmailValidator class overrides the AddRules method from the base class. This method is responsible for adding validation rules to the RuleContext object. Inside the AddRules method, the code first retrieves the value of the subject using the GetValue method. It assigns this value to a variable called value. Next, the code checks if the value is null, empty, or consists only of whitespace using the string.IsNullOrWhiteSpace method. If it is, it adds a broken rule to the RuleContext object with the key "Email" and the message "Email is required". Then, the code checks if the value is not null, not empty, and is a valid email address using the EmailHelper.IsValidEmail method. If it is not a valid email address, it adds another broken rule to the RuleContext object with the key "Email" and the message "Email is invalid". Overall, this code ensures that the email address provided in the subject is not null, empty, or invalid, and adds appropriate broken rules to the RuleContext object if any of these conditions are not met.
public class EmailValidator : AbstractRuleValidator<ValueObject<string>>
{
public EmailValidator(ValueObject<string> subject) : base(subject)
{
}
public override void AddRules(RuleContext context)
{
var value = Subject!.GetValue();
if (string.IsNullOrWhiteSpace(value))
{
AddBrokenRule("Email", "Email is required");
}
if (!string.IsNullOrWhiteSpace(value) && !EmailHelper.IsValidEmail(value))
{
AddBrokenRule("Email", "Email is invalid");
}
}
}
Subscribe to my newsletter
Read articles from Alberto Arroyo Raygada directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Alberto Arroyo Raygada
Alberto Arroyo Raygada
Dynamic and motivated professional with a proven record of generating and building software, from concept to implementation, designing architectures, and coaching teams to technical improvement skilled in building cross-functional teams, demonstrating communication skills, and making critical decisions during challenges. I am an adaptable and transformational leader who can work independently, create effective presentations, and develop opportunities that further establish organizational goals. Technical Skills C#, NET Core, NET, NodeJS, NestJS, JavaScript, TypeScript, React, NextJS, Tailwind, Langchain, TFS, Azure DevOps, Kubernetes, Azure and AWS. Business and Management Skills Experience with Agile Projects, Senior Analyst, Technical Lead, Lead Process, Software Manager, and Technical Manager, experience working on Supply Chain, WMS, TMS, YMS, CMMS, BMS, OMS, Foreign Trade, Retail, e-commerce and Direct Sales, Lead Management for Assurance, FinTech and Learning Management.