I have an aggregate root Products
which contains a list of entities Selection
, which in turn contains a list of entities called Features
.
- The aggregate root
Product
has an identity of just name - The entity
Selection
has an identity of name (and its corresponding Product identity) - The entity
Feature
has an identity of name (and also it's corresponding Selection identity)
Where the identities for the entities are built as follows:
var productId = new ProductId("dedisvr");
var selectionId = new SelectionId("os",productId);
var featureId = new FeatureId("windowsstd",selectionId);
Note that the dependent identity takes the identity of the parent as part of a composite.
The idea is that this would form a product part number which can be identified by a specific feature in a selection, i.e. the ToString()
for the above featureId object would return dedisvr-os-windowsstd
.
Everything exists within the Product aggregate where business logic is used to enforce invariant on relationships between selections and features. In my domain, it doesn't make sense for a feature to exist without a selection, and selection without an associated product.
When querying the product for associated features, the Feature object is returned but the C# internal
keyword is used to hide any methods that could mutate the entity, and thus ensure the entity is immutable to the calling application service (in a different assembly from domain code).
These two above assertions are provided for by the two functions:
class Product
{
/* snip a load of other code */
public void AddFeature(FeatureIdentity identity, string description, string specification, Prices prices)
{
// snip...
}
public IEnumerable<Feature> GetFeaturesMemberOf(SelectionIdentity identity);
{
// snip...
}
}
I have a aggregate root called Service order, this will contain a ConfigurationLine which will reference the Feature
within the Product
aggregate root by FeatureId
. This may be in an entirely different bounded context.
Since the FeatureId contains the fields SelectionId
and ProductId
I will know how to navigate to the feature via the aggregate root.
My questions are:
Composite identities formed with identity of parent - good or bad practice?
In other sample DDD code where identities are defined as classes, I haven't seen yet any composites formed of the local entity id and its parent identity. I think it is a nice property, since we can always navigate to that entity (always through the aggregate root) with knowledge of the path to get there (Product -> Selection -> Feature).
Whilst my code with the composite identity chain with the parent makes sense and allows me to navigate to the entity via the root aggregate, not seeing other code examples where identities are formed similarly with composites makes me very nervous - any reason for this or is this bad practice?
References to internal entities - transient or long term?
The bluebook mentions references to entities within an aggregate are acceptable but should only be transient (within a code block). In my case I need to store references to these entities for use in future, storing is not transient.
However the need to store this reference is for reporting and searching purposes only, and even if i did want to retrieve the child entity bu navigate via the root, the entities returned are immutable so I don't see any harm can be done or invariants broken.
Is my thinking correct and if so why is it mentioned keep child entity references transient?
Source code is below:
public class ProductIdentity : IEquatable<ProductIdentity>
{
readonly string name;
public ProductIdentity(string name)
{
this.name = name;
}
public bool Equals(ProductIdentity other)
{
return this.name.Equals(other.name);
}
public string Name
{
get { return this.name; }
}
public override int GetHashCode()
{
return this.name.GetHashCode();
}
public SelectionIdentity NewSelectionIdentity(string name)
{
return new SelectionIdentity(name, this);
}
public override string ToString()
{
return this.name;
}
}
public class SelectionIdentity : IEquatable<SelectionIdentity>
{
readonly string name;
readonly ProductIdentity productIdentity;
public SelectionIdentity(string name, ProductIdentity productIdentity)
{
this.productIdentity = productIdentity;
this.name = name;
}
public bool Equals(SelectionIdentity other)
{
return (this.name == other.name) && (this.productIdentity == other.productIdentity);
}
public override int GetHashCode()
{
return this.name.GetHashCode();
}
public override string ToString()
{
return this.productIdentity.ToString() + "-" + this.name;
}
public FeatureIdentity NewFeatureIdentity(string name)
{
return new FeatureIdentity(name, this);
}
}
public class FeatureIdentity : IEquatable<FeatureIdentity>
{
readonly SelectionIdentity selection;
readonly string name;
public FeatureIdentity(string name, SelectionIdentity selection)
{
this.selection = selection;
this.name = name;
}
public bool BelongsTo(SelectionIdentity other)
{
return this.selection.Equals(other);
}
public bool Equals(FeatureIdentity other)
{
return this.selection.Equals(other.selection) && this.name == other.name;
}
public SelectionIdentity SelectionId
{
get { return this.selection; }
}
public string Name
{
get { return this.name; }
}
public override int GetHashCode()
{
return this.name.GetHashCode();
}
public override string ToString()
{
return this.SelectionId.ToString() + "-" + this.name;
}
}
Truck
contains a collection ofTyre
instances. If, say, aConditionMonitor
is passed aTyre
instance from aTruck
object then thatTyre
instance should be transient. AnyTruck
holding on to its own entities forever is fine. – Galliwasp