Composable Repository Interfaces

An alternative to a single IRepository<T> (see Repository Pattern): split repository capabilities into small interfaces, each gated by a matching marker interface on the entity. An entity only gets the operations its marker interfaces say it supports — there's no DeleteAsync sitting unused on an entity that's never deletable.

Entity capability markers

Plain marker interfaces in the Domain layer. An entity implements whichever combination it needs.

IEntities/*.cs
public interface IEntity
{
    Guid Id { get; set; }
}

public interface ICreateable
{
    DateTime CreatedAt { get; set; }
}

public interface IUpdatable
{
    DateTime UpdatedAt { get; set; }
}

public interface ISoftDeletable
{
    bool SoftDeleted { get; set; }
    DateTime SoftDeletedAt { get; set; }
}
Product.cs — picks the capabilities it needs
public class Product : IEntity, ICreateable, IUpdatable, ISoftDeletable
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public bool SoftDeleted { get; set; }
    public DateTime SoftDeletedAt { get; set; }

    public string Name { get; set; } = "";
    // ...
}

One repository interface per capability

Each generic constraint (where T : ICreateable, etc.) means the interface — and its EF implementation — only compiles against entities that actually support it.

IEfRepository.cs + friends
public interface IEfRepository<T> where T : class, IEntity
{
    Task<List<T>> GetAllAsync();
    Task<T?> GetByIdAsync(Guid id);
    Task SaveChangesAsync();
}

public interface IEfCreatableRepository<T> where T : class, ICreateable
{
    Task AddAsync(T entity);
}

public interface IEfUpdatableRepository<T> where T : class, IUpdatable
{
    Task UpdateAsync(T entity);
}

public interface IEfSoftDeletableRepository<T> where T : class, ISoftDeletable
{
    Task SoftDeleteAsync(T entity);
}

Implementations stamp the timestamp themselves

Because each implementation is scoped to one concern, it's the natural place to stamp CreatedAt/UpdatedAt/SoftDeletedAt — callers never set these manually, so they can't forget to.

EfCreatableRepository.cs / EfSoftDeletableRepository.cs
public class EfCreatableRepository<T> : IEfCreatableRepository<T> where T : class, ICreateable
{
    private readonly DbSet<T> _dbSet;
    private readonly AppDbContext _context;

    public EfCreatableRepository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task AddAsync(T entity)
    {
        entity.CreatedAt = DateTime.UtcNow;
        _dbSet.Add(entity);
        await _context.SaveChangesAsync();
    }
}

public class EfSoftDeletableRepository<T> : IEfSoftDeletableRepository<T> where T : class, ISoftDeletable
{
    private readonly DbSet<T> _dbSet;
    private readonly AppDbContext _context;

    public EfSoftDeletableRepository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task SoftDeleteAsync(T entity)
    {
        entity.SoftDeletedAt = DateTime.UtcNow;
        entity.SoftDeleted = true;
        _dbSet.Update(entity);
        await _context.SaveChangesAsync();
    }
}

A concrete repository composes what it needs

A feature-specific repository interface inherits exactly the capability interfaces its entity supports, then the implementation composes the generic pieces (often by inheriting more than one, or by injecting them) rather than reimplementing CRUD by hand.

IProductRepository.cs
public interface IProductRepository :
    IEfRepository<Product>,
    IEfCreatableRepository<Product>,
    IEfUpdatableRepository<Product>,
    IEfSoftDeletableRepository<Product>
{
    Task<List<Product>> GetAllProductsAsync(ProductPaginationDto pagination);
    Task<int> GetNewBarcode();
}
Trade-off versus a single IRepository<T>: more files and more generic constraints up front, in exchange for the compiler stopping you from calling DeleteAsync on something that was never meant to be deletable, and from forgetting to update a timestamp.