docs/en/framework/architecture/domain-driven-design/entities.md
//[doc-seo]
{
"Description": "Learn about entities in DDD, their structure, and best practices for using GUID keys in the ABP Framework for effective data modeling."
}
Entities are one of the core concepts of DDD (Domain Driven Design). Eric Evans describes it as "An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity".
An entity is generally mapped to a table in a relational database.
Entities are derived from the Entity<TKey> class as shown below:
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
}
If you do not want to derive your entity from the base
Entity<TKey>class, you can directly implementIEntity<TKey>interface.
Entity<TKey> class just defines an Id property with the given primary key type, which is Guid in the example above. It can be other types like string, int, long, or whatever you need.
If your entity's Id type is Guid, there are some good practices to implement:
private or protected empty constructor. This is used while your database provider reads your entity from the database (on deserialization).Guid.NewGuid() to set the Id! Use the IGuidGenerator service while passing the Id from the code that creates the entity. IGuidGenerator optimized to generate sequential GUIDs, which is critical for clustered indexes in the relational databases.An example entity:
public class Book : Entity<Guid>
{
public string Name { get; set; }
public float Price { get; set; }
protected Book()
{
}
public Book(Guid id)
: base(id)
{
}
}
Example usage in an application service:
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book> _bookRepository;
public BookAppService(IRepository<Book> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task CreateAsync(CreateBookDto input)
{
await _bookRepository.InsertAsync(
new Book(GuidGenerator.Create())
{
Name = input.Name,
Price = input.Price
}
);
}
}
BookAppService injects the default repository for the book entity and uses its InsertAsync method to insert a Book to the database.GuidGenerator is type of IGuidGenerator which is a property defined in the ApplicationService base class. ABP defines such frequently used base properties as pre-injected for you, so you don't need to manually inject them.Some entities may need to have composite keys. In that case, you can derive your entity from the non-generic Entity class. Example:
public class UserRole : Entity
{
public Guid UserId { get; set; }
public Guid RoleId { get; set; }
public DateTime CreationTime { get; set; }
public UserRole()
{
}
public override object[] GetKeys()
{
return new object[] { UserId, RoleId };
}
}
For the example above, the composite key is composed of UserId and RoleId. For a relational database, it is the composite primary key of the related table. Entities with composite keys should implement the GetKeys() method as shown above.
Notice that you also need to define keys of the entity in your object-relational mapping (ORM) configuration. See the Entity Framework Core integration document for example.
Also note that Entities with Composite Primary Keys cannot utilize the
IRepository<TEntity, TKey>interface since it requires a single Id property. However, you can always useIRepository<TEntity>. See repositories documentation for more.
Entity.EntityEquals(...) method is used to check if two Entity Objects are equals.
Example:
Book book1 = ...
Book book2 = ...
if (book1.EntityEquals(book2)) //Check equality
{
...
}
IKeyedObject InterfaceABP entities implement the IKeyedObject interface, which provides a way to get the entity's primary key as a string:
public interface IKeyedObject
{
string? GetObjectKey();
}
The GetObjectKey() method returns a string representation of the entity's primary key. For entities with a single key (like Entity<Guid> or Entity<int>), it returns the Id property converted to a string. For entities with composite keys, it returns the keys combined with a comma separator.
This interface is particularly useful for scenarios where you need to identify an entity by its key in a type-agnostic way, such as:
Since all ABP entities implement this interface through the IEntity interface, you can use GetObjectKey() on any entity without additional implementation.
See the Resource-Based Authorization documentation for a practical example of using
IKeyedObjectwith the permission system.
"Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it's useful to treat the order (together with its line items) as a single aggregate." (see the full description)
AggregateRoot<TKey> class extends the Entity<TKey> class. So, it also has an Id property by default.
Notice that ABP creates default repositories only for aggregate roots by default. However, it's possible to include all entities. See the repositories documentation for more.
ABP does not force you to use aggregate roots, you can in fact use the Entity class as defined before. However, if you want to implement the Domain Driven Design and want to create aggregate root classes, there are some best practices you may want to consider:
Id. Do not reference it by its navigation property.See the entity design best practice guide if you want to implement DDD in your application.
This is a full sample of an aggregate root with a related sub-entity collection:
public class Order : AggregateRoot<Guid>
{
public virtual string ReferenceNo { get; protected set; }
public virtual int TotalItemCount { get; protected set; }
public virtual DateTime CreationTime { get; protected set; }
public virtual List<OrderLine> OrderLines { get; protected set; }
protected Order()
{
}
public Order(Guid id, string referenceNo)
{
Check.NotNull(referenceNo, nameof(referenceNo));
Id = id;
ReferenceNo = referenceNo;
OrderLines = new List<OrderLine>();
}
public void AddProduct(Guid productId, int count)
{
if (count <= 0)
{
throw new ArgumentException(
"You can not add zero or negative count of products!",
nameof(count)
);
}
var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);
if (existingLine == null)
{
OrderLines.Add(new OrderLine(this.Id, productId, count));
}
else
{
existingLine.ChangeCount(existingLine.Count + count);
}
TotalItemCount += count;
}
}
public class OrderLine : Entity
{
public virtual Guid OrderId { get; protected set; }
public virtual Guid ProductId { get; protected set; }
public virtual int Count { get; protected set; }
protected OrderLine()
{
}
internal OrderLine(Guid orderId, Guid productId, int count)
{
OrderId = orderId;
ProductId = productId;
Count = count;
}
internal void ChangeCount(int newCount)
{
Count = newCount;
}
public override object[] GetKeys()
{
return new Object[] {OrderId, ProductId};
}
}
If you do not want to derive your aggregate root from the base
AggregateRoot<TKey>class, you can directly implement theIAggregateRoot<TKey>interface.
Order is an aggregate root with Guid type Id property. It has a collection of OrderLine entities. OrderLine is another entity with a composite primary key (OrderId and ProductId).
While this example may not implement all the best practices of an aggregate root, it still follows some good practices:
Order has a public constructor that takes minimal requirements to construct an Order instance. So, it's not possible to create an order without an id and reference number. The protected/private constructor is only necessary to deserialize the object while reading from a data source.OrderLine constructor is internal, so it is only allowed to be created by the domain layer. It's used inside of the Order.AddProduct method.Order.AddProduct implements the business rule to add a product to an order.protected setters. This is to prevent the entity from arbitrary changes from outside of the entity. For example, it would be dangerous to set TotalItemCount without adding a new product to the order. Its value is maintained by the AddProduct method.ABP does not force you to apply any DDD rule or patterns. However, it tries to make it possible and easier when you do want to apply them. The documentation also follows the same principle.
While it's not common (and not suggested) for aggregate roots, it is in fact possible to define composite keys in the same way as defined for the mentioned entities above. Use non-generic AggregateRoot base class in that case.
AggregateRoot class implements the IHasExtraProperties and IHasConcurrencyStamp interfaces which brings two properties to the derived class. IHasExtraProperties makes the entity extensible (see the Extra Properties section below) and IHasConcurrencyStamp adds a ConcurrencyStamp property that is managed by the ABP to implement the optimistic concurrency. In most cases, these are wanted features for aggregate roots.
However, if you don't need these features, you can inherit from the BasicAggregateRoot<TKey> (or BasicAggregateRoot) for your aggregate root.
There are some properties like CreationTime, CreatorId, LastModificationTime... which are very common in all applications. ABP provides some interfaces and base classes to standardize these properties and also sets their values automatically.
There are a lot of auditing interfaces, so you can implement the one that you need.
While you can manually implement these interfaces, you can use the base classes defined in the next section to simplify it.
IHasCreationTime defines the following properties:
CreationTimeIMayHaveCreator defines the following properties:
CreatorIdICreationAuditedObject inherits from the IHasCreationTime and the IMayHaveCreator, so it defines the following properties:
CreationTimeCreatorIdIHasModificationTime defines the following properties:
LastModificationTimeIModificationAuditedObject extends the IHasModificationTime and adds the LastModifierId property. So, it defines the following properties:
LastModificationTimeLastModifierIdIAuditedObject extends the ICreationAuditedObject and the IModificationAuditedObject, so it defines the following properties:
CreationTimeCreatorIdLastModificationTimeLastModifierIdISoftDelete (see the data filtering document) defines the following properties:
IsDeletedIHasDeletionTime extends the ISoftDelete and adds the DeletionTime property. So, it defines the following properties:
IsDeletedDeletionTimeIDeletionAuditedObject extends the IHasDeletionTime and adds the DeleterId property. So, it defines the following properties:
IsDeletedDeletionTimeDeleterIdIFullAuditedObject inherits from the IAuditedObject and the IDeletionAuditedObject, so it defines the following properties:
CreationTimeCreatorIdLastModificationTimeLastModifierIdIsDeletedDeletionTimeDeleterIdOnce you implement any of the interfaces, or derive from a class defined in the next section, ABP automatically manages these properties wherever possible.
Implementing
ISoftDelete,IDeletionAuditedObjectorIFullAuditedObjectmakes your entity soft-delete. See the data filtering document to learn about the soft-delete pattern.
While you can manually implement any of the interfaces defined above, it is suggested to inherit from the base classes defined here:
CreationAuditedEntity<TKey> and CreationAuditedAggregateRoot<TKey> implement the ICreationAuditedObject interface.AuditedEntity<TKey> and AuditedAggregateRoot<TKey> implement the IAuditedObject interface.FullAuditedEntity<TKey> and FullAuditedAggregateRoot<TKey> implement the IFullAuditedObject interface.All these base classes also have non-generic versions to take AuditedEntity and FullAuditedAggregateRoot to support the composite primary keys.
All these base classes also have ...WithUser pairs, like FullAuditedAggregateRootWithUser<TUser> and FullAuditedAggregateRootWithUser<TKey, TUser>. This makes possible to add a navigation property to your user entity. However, it is not a good practice to add navigation properties between aggregate roots, so this usage is not suggested (unless you are using an ORM, like EF Core, that well supports this scenario and you really need it - otherwise remember that this approach doesn't work for NoSQL databases like MongoDB where you must truly implement the aggregate pattern). Also, if you add navigation properties to the AppUser class that comes with the startup template, consider to handle (ignore/map) it on the migration dbcontext (see the EF Core migration document).
ABP provides a Distributed Entity Cache System for caching entities. It is useful if you want to use caching for quicker access to the entity rather than repeatedly querying it from the database.
It's designed as read-only and automatically invalidates a cached entity if the entity is updated or deleted.
See the Entity Cache documentation for more information.
ABP defines the IHasEntityVersion interface for automatic versioning of your entities. It only provides a single EntityVersion property, as shown in the following code block:
public interface IHasEntityVersion
{
int EntityVersion { get; }
}
If you implement the IHasEntityVersion interface, ABP automatically increases the EntityVersion value whenever you update your entity. The initial EntityVersion value will be 0, when you first create an entity and save to the database.
ABP can not increase the version if you directly execute SQL
UPDATEcommands in the database. It is your responsibility to increase theEntityVersionvalue in that case. Also, if you are using the aggregate pattern and change sub-collections of an aggregate root, it is your responsibility if you want to increase the version of the aggregate root object.
ABP defines the IHasExtraProperties interface that can be implemented by an entity to be able to dynamically set and get properties for the entity. AggregateRoot base class already implements the IHasExtraProperties interface. If you've derived from this class (or one of the related audit class defined above), you can directly use the API.
These extension methods are the recommended way to get and set data for an entity. Example:
public class ExtraPropertiesDemoService : ITransientDependency
{
private readonly IIdentityUserRepository _identityUserRepository;
public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository)
{
_identityUserRepository = identityUserRepository;
}
public async Task SetTitle(Guid userId, string title)
{
var user = await _identityUserRepository.GetAsync(userId);
//SET A PROPERTY
user.SetProperty("Title", title);
await _identityUserRepository.UpdateAsync(user);
}
public async Task<string> GetTitle(Guid userId)
{
var user = await _identityUserRepository.GetAsync(userId);
//GET A PROPERTY
return user.GetProperty<string>("Title");
}
}
GetProperty returns null if given property was not set before.Title here).It would be a good practice to define a constant for the property name to prevent typo errors. It would be even a better practice to define extension methods to take the advantage of the intellisense. Example:
public static class IdentityUserExtensions
{
private const string TitlePropertyName = "Title";
public static void SetTitle(this IdentityUser user, string title)
{
user.SetProperty(TitlePropertyName, title);
}
public static string GetTitle(this IdentityUser user)
{
return user.GetProperty<string>(TitlePropertyName);
}
}
Then you can directly use user.SetTitle("...") and user.GetTitle() for an IdentityUser object.
HasProperty is used to check if the object has a property set before.RemoveProperty is used to remove a property from the object. You can use this instead of setting a null value.IHasExtraProperties interface requires to define a Dictionary<string, object> property, named ExtraProperties, for the implemented class.
So, you can directly use the ExtraProperties property to use the dictionary API, if you like. However, SetProperty and GetProperty methods are the recommended ways since they also check for nulls.
The way to store this dictionary in the database depends on the database provider you're using.
ExtraProperties field as a JSON string (that means all extra properties stored in a single database table field). Serializing to JSON and deserializing from the JSON are automatically done by the ABP using the value conversions system of the EF Core.ObjectExtensionManager to define a separate table field for a desired extra property. Properties those are not configured through the ObjectExtensionManager will continue to use a single JSON field as described above. This feature is especially useful when you are using a pre-built application module and want to extend its entities. See the EF Core integration document to learn how to use the ObjectExtensionManager.Extra Properties system is especially useful if you are using a re-usable module that defines an entity inside and you want to get/set some data related to this entity in an easy way.
You typically don't need to use this system for your own entities, because it has the following drawbacks:
IHasExtraProperties is not restricted to be used with entities. You can implement this interface for any kind of class and use the GetProperty, SetProperty and other related methods.