A repository represents all objects of a certain type as a conceptual set (usually emulated). It acts like a collection, except with more elaborate querying capability. [...] For each type of object that needs global access, create an object that can provide the illusion of an in-memory collection of all objects of that type.
Eric Evans - Domain Driven Design
Most repository patterns contain many query methods, and they are often nearly identical to the DbContext class. Unlike the norm, the Eiffel framework follows a collection-oriented repository approach."
Each repository should expose a single aggregate, and the maintenance of the entities or value objects should be handled by the owner aggregate.
/// <summary>
/// IRepository interface
/// </summary>
/// <typeparam name="TAggregate">The type of aggregate</typeparam>
/// <typeparam name="TIdentity">The tpye of aggregate identity</typeparam>
/// <typeparam name="TKey">The type of database primary key</typeparam>
public interface IRepository<TAggregate, TIdentity, TKey>
where TKey : struct, IEquatable<TKey>
where TIdentity : class
where TAggregate : Aggregate<TIdentity, TKey>
{
/// <summary>
/// Gets record from database
/// </summary>
/// <param name="identity">Aggregate</param>
/// <param name="cancellationToken">Cancellation token</param>
Task<TAggregate> GetAsync(TIdentity identity, CancellationToken cancellationToken = default);
/// <summary>
/// Adds record to database
/// </summary>
/// <param name="aggregate">Aggregate identifier</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Record from database</returns>
Task AddAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
/// <summary>
/// Commit changes to database
/// </summary>
/// <param name="aggregate">Aggregate</param>
/// <param name="cancellationToken">Cancellation token</param>
Task SaveChangesAsync(TAggregate aggregate, CancellationToken cancellationToken = default);
}