Repositories¶
Repository Interfaces¶
- Do define repository interfaces in the domain layer.
- Do define a repository interface (like
IProjectRepository
) and create its corresponding implementations for each aggregate root.- Do always use the created repository interface from the application code.
- Do not use generic repository interfaces (like
IRepository<Project, Guid>
) from the application code. - Do not use
IQueryable<TEntity>
features in the application code (domain, application... layers).
For the example aggregate root:
public class Project : AuditedAggregateRoot<Guid>
{
//...
}
Define the repository interface as below:
public interface IProjectRepository : IBasicRepository<Project, Guid>, IReadOnlyRepository<Project, Guid>
{
//...
}
- Do inherit the repository interface from
IBasicRepository<TEntity, TKey>
(as normally) or a lower-featured interface, likeIReadOnlyRepository<TEntity, TKey>
(if it's needed). - Do not inherit the repository interface from the
IRepository<TEntity, TKey>
interface. Because it inherits theIQueryable
and the repository should not exposeIQueryable
to the application. - Do not define repositories for entities those are not aggregate roots.
Repository Methods¶
- Do define all repository methods as asynchronous.
- Do add an optional
cancellationToken
parameter to every method of the repository. Example:
Task<Project> FindAsync(
[NotNull] string name,
CancellationToken cancellationToken = default
);
- Do add an optional
bool includeDetails = true
parameter (default value istrue
) for every repository method which returns a single entity. Example:
Task<Project> FindAsync(
[NotNull] string name,
bool includeDetails = true,
CancellationToken cancellationToken = default
);
This parameter will be implemented for ORMs to eager load sub collections of the entity.
- Do add an optional
bool includeDetails = false
parameter (default value isfalse
) for every repository method which returns a list of entities. Example:
Task<List<Project>> GetListByNameAsync(
[NotNull] string name,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
- Do not create composite classes to combine entities to get from repository with a single method call.
Examples: UserWithRoles, UserWithTokens, UserWithRolesAndTokens. Instead, properly useincludeDetails
option to add all details of the entity when needed. - Avoid to create projection classes for entities to get less property of an entity from the repository.
Example: Avoid to create BasicUserView class to select a few properties needed for the use case needs. Instead, directly use the aggregate root class. However, there may be some exceptions for this rule, where:- Performance is so critical for the use case and getting the whole aggregate root highly impacts the performance.
Aggregate should reference to other aggregates only by their Id. That means you can not add navigation properties to other aggregates.
-
This rule makes it possible to implement the serializability principle.
-
It also prevents different aggregates manipulate each other and leaking business logic of an aggregate to one another.
For the example aggregate root has relation with other aggregate:
public class Project : AuditedAggregateRoot<Guid>
{
public virtual string Name { get; protected set; }
public virtual Guid BaseModelId { get; protected set; } // Navigation property for "Base Model" Aggreate Root
}
public class BaseModel : AuditedAggregateRoot<Guid>
{
public virtual string SchemaName { get; protected set; }
}
- Do define
GetWithNavigation
andGetListWithNavigation
repository methods if there is relation between two or more aggregate
#region Project With Navigation
Task<ProjectWithNavigationProperties> GetWithNavigationPropertiesAsync(
Guid id,
CancellationToken cancellationToken = default
);
Task<List<ProjectWithNavigationProperties>> GetListWithNavigationPropertiesAsync(
string name = null,
Guid? baseModelId = null,
string sorting = null,
int maxResultCount = 100,
int skipCount = 0,
CancellationToken cancellationToken = default
);
#endregion