Rulegeneral
Repositories Rule
Rules for DDD repositories, including tenant scoping, interface conventions, and use of Entity Framework
DDD Repositories
Guidelines for implementing DDD repositories in the backend, including structure, interface conventions, and Entity Framework mapping.
Implementation
- Create repositories alongside their corresponding aggregates in
/[scs-name]/Core/Features/[Feature]/Domain - Create a public sealed class implementation using a primary constructor
- All implementations must inherit from
RepositoryBase<TAggregate, TId> - Create an interface that extends
IBaseRepository<TAggregate, TId>orICrudRepository<TAggregate, TId>:- Use
IBaseRepositorywhen you don't need all CRUD operations - Only include methods needed for your specific aggregate
- Use
- Only return Aggregates or custom projections—never Entities or Value Objects
- Never return
[PublicAPI]response DTOs (repositories return domain objects; mapping to DTOs happens in query handlers) - Keep repositories focused on persistence operations, not business logic
- Repositories are automatically registered in the DI container
- Aggregates with
ITenantScopedEntityare automatically filtered by tenant using EF Core query filters:- In rare cases, disable this using
IgnoreQueryFilters(e.g., looking up anonymous user by email) - If using
IgnoreQueryFilters, add anUnfilteredAsyncsuffix and an XML comment warning about disabled filters
- In rare cases, disable this using
- Use
IEntityTypeConfiguration<TAggregate>for EF Core model configuration - Map strongly typed IDs in EF Core configurations using:
MapStronglyTypedUuidfor ULIDsMapStronglyTypedLongIdfor long IDsMapStronglyTypedGuidfor GUIDs
- Updating entities doesn't belong in repositories—fetch the aggregate in commands, update it, then save via the repository
- Never add
.AsTracking()to queries—userepository.Update()which handles tracking internally - Never do N+1 queries
- Don't register repositories in DI—SharedKernel registers them automatically
- Don't add DbSets to DbContext—RepositoryBase handles this automatically
Examples
// ✅ DO: Only include needed methods, use correct base interface, and inherit RepositoryBase
public interface ILoginRepository : IAppendRepository<Login, LoginId> // ✅ DO: Use only needed base interface
{
void Update(Login aggregate); // ✅ DO: Add only needed methods
}
public sealed class LoginRepository(AccountManagementDbContext accountManagementDbContext) // ✅ DO: Use sealed class and primary constructor
: RepositoryBase<Login, LoginId>(accountManagementDbContext), ILoginRepository;
// ❌ DON'T: Use ICrudRepository if not all CRUD ops needed, or return DTOs
internal interface IBadLoginRepository : ICrudRepository<Login, LoginId> // ❌ DON'T: Make repositories internal
{
Task<LoginDto> GetDto(LoginId id); // ❌ DON'T: Return DTOs from repositories, map entities in the query
}
// ✅ DO: Example with a custom query method
public interface IEmailConfirmationRepository : IAppendRepository<EmailConfirmation, EmailConfirmationId>
{
EmailConfirmation[] GetByEmail(string email); // ✅ DO: Custom query method allowed
}
public sealed class EmailConfirmationRepository(AccountManagementDbContext accountManagementDbContext) // ✅ DO: Use sealed class and inherit RepositoryBase
: RepositoryBase<EmailConfirmation, EmailConfirmationId>(accountManagementDbContext), IEmailConfirmationRepository
{
public EmailConfirmation[] GetByEmail(string email)
=> DbSet.Where(ec => !ec.Completed && ec.Email == email.ToLowerInvariant()).ToArray(); // ✅ DO: Implement custom query
}
public sealed class AccountManagementDbContext(DbContextOptions<AccountManagementDbContext> options, IExecutionContext executionContext)
: SharedKernelDbContext<AccountManagementDbContext>(options, executionContext)
{
public DbSet<EmailConfirmation> EmailConfirmations => Set<EmailConfirmation>(); // ❌ DON'T: Add DbSet<T> to DbContext, this is automatically handled in RepositoryBase
}
Use of IgnoreQueryFilters
If you use .IgnoreQueryFilters(), the repository method must have an UnfilteredAsync suffix and an XML comment warning that this is dangerous and disables tenant and soft-delete filters.
/// <summary> // ✅ DO: Add XML comment explaining why ignoring query filters is acceptable
/// Retrieves a user by email without applying tenant query filters.
/// This method should only be used during the login processes where tenant context is not yet established.
/// </summary>
public async Task<User?> GetUserByEmailUnfilteredAsync(string email, CancellationToken cancellationToken) // ✅ DO: Add `Unfiltered` to the surffix
{
return await DbSet.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Email == email.ToLowerInvariant(), cancellationToken); // ✅ DO: Use .IgnoreQueryFilters() only in rare cases, with UnfilteredAsync suffix and XML comment
}
// ❌ DON'T: Use .IgnoreQueryFilters() without UnfilteredAsync suffix or without an XML warning comment
public async Task<User?> GetUserByEmail(string email, CancellationToken cancellationToken)
{
return await DbSet.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Email == email.ToLowerInvariant(), cancellationToken); // ❌ Missing UnfilteredAsync suffix and XML comment
}