Repository Best Practices & Conventions
Repository Interfaces
- Do define repository interfaces in the domain layer.
- Do define a repository interface (like
IIdentityUserRepository
) 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<IdentityUser, 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 IdentityUser : AggregateRoot<Guid>
{
//...
}
Define the repository interface as below:
public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
{
//...
}
- 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 inherit the repository interface from
IBasicRepository<TEntity, TKey>
(as normally) or a lower-featured interface, likeIReadOnlyRepository<TEntity, TKey>
(if it's needed). - 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<IdentityUser> FindByNormalizedUserNameAsync(
[NotNull] string normalizedUserName,
CancellationToken cancellationToken = default
);
- Do create a synchronous extension method for each asynchronous repository method. Example:
public static class IdentityUserRepositoryExtensions
{
public static IdentityUser FindByNormalizedUserName(
this IIdentityUserRepository repository,
[NotNull] string normalizedUserName)
{
return AsyncHelper.RunSync(
() => repository.FindByNormalizedUserNameAsync(normalizedUserName)
);
}
}
This will allow synchronous code to use the repository methods easier.
- Do add an optional
bool includeDetails = true
parameter (default value istrue
) for every repository method which returns a single entity. Example:
Task<IdentityUser> FindByNormalizedUserNameAsync(
[NotNull] string normalizedUserName,
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<IdentityUser>> GetListByNormalizedRoleNameAsync(
string normalizedRoleName,
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 use
includeDetails
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.