docs/en/framework/architecture/best-practices/entities.md
//[doc-seo]
{
"Description": "Explore best practices for implementing Aggregate Root and Entity classes in your applications using Domain-Driven Design principles."
}
This document offers best practices for implementing Aggregate Root and Entity classes in your modules and applications based on Domain-Driven-Design principles.
Ensure you've read the Entities document first.
Every aggregate root is also an entity. So, these rules are valid for aggregate roots too unless aggregate root rules override them.
public, internal or protected internal based on the requirements. If it's not public, the entity is expected to be created by a domain service.Guid keys inside the constructor. Get it as a parameter, so the calling code will use IGuidGenerator to generate a new Guid value.protected parameterless constructor to be compatible with ORMs.virtual (except private methods, obviously). Because some ORMs and dynamic proxy tools require it.private, protected, internal or protected internal setter where it is needed to protect the entity consistency and validity.public , internal or protected internal (virtual) methods to change the properties (with non-public setters) if necessary.this) from the setter methods.AggregateRoot<TKey> or one of the audited classes (CreationAuditedAggregateRoot<TKey>, AuditedAggregateRoot<TKey> or FullAuditedAggregateRoot<TKey>) based on requirements.public class Issue : FullAuditedAggregateRoot<Guid> //Using Guid as the key/identifier
{
public virtual string Title { get; private set; } //Changed using the SetTitle() method
public virtual string Text { get; set; } //Can be directly changed. null values are allowed
public virtual Guid? MilestoneId { get; set; } //Reference to another aggregate root
public virtual bool IsClosed { get; private set; }
public virtual IssueCloseReason? CloseReason { get; private set; } //Just an enum type
public virtual Collection<IssueLabel> Labels { get; protected set; } //Sub collection
protected Issue()
{
/* This constructor is for ORMs to be used while getting the entity from database.
* - No need to initialize the Labels collection
since it will be overrided from the database.
- It's protected since proxying and deserialization tools
may not work with private constructors.
*/
}
//Primary constructor
public Issue(
Guid id, //Get Guid value from the calling code
[NotNull] string title, //Indicate that the title can not be null.
string text = null,
Guid? milestoneId = null) //Optional argument
{
Id = id;
Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //Validate
Text = text;
MilestoneId = milestoneId;
Labels = new Collection<IssueLabel>(); //Always initialize the collection
}
public virtual Issue SetTitle([NotNull] string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //Validate
return this;
}
/* AddLabel & RemoveLabel methods manages the Labels collection
* in a safe way (prevents adding the same label twice) */
public virtual Issue AddLabel(Guid labelId)
{
if (Labels.Any(l => l.LabelId == labelId))
{
return;
}
Labels.Add(new IssueLabel(Id, labelId));
return this;
}
public virtual Issue RemoveLabel(Guid labelId)
{
Labels.RemoveAll(l => l.LabelId == labelId);
return this;
}
/* Close & ReOpen methods protect the consistency
* of the IsClosed and the CloseReason properties. */
public virtual void Close(IssueCloseReason reason)
{
IsClosed = true;
CloseReason = reason;
}
public virtual void ReOpen()
{
IsClosed = false;
CloseReason = null;
}
}
public class IssueLabel : Entity
{
public virtual Guid IssueId { get; private set; }
public virtual Guid LabelId { get; private set; }
protected IssueLabel()
{
}
public IssueLabel(Guid issueId, Guid labelId)
{
IssueId = issueId;
LabelId = labelId;
}
}