EfCoreKit provides a hierarchy of ready-made base classes so you don't have to repeat the same property declarations across every entity. Pick the one that matches the features you need.
BaseEntity<TKey>
└── AuditableEntity<TKey> (+ CreatedAt/By, UpdatedAt/By)
└── SoftDeletableEntity<TKey> (+ IsDeleted, DeletedAt/By)
└── FullEntity<TKey> (+ RowVersion)
Each level adds the interface properties for the corresponding feature. All levels have an int-key convenience alias (e.g. BaseEntity = BaseEntity<int>).
public abstract class BaseEntity<TKey>
{
public TKey Id { get; set; } = default!;
}
// Shorthand for int PK
public abstract class BaseEntity : BaseEntity<int> { }Use when you want a strongly-typed Id property and nothing else from the library.
public class Tag : BaseEntity // int Id
public class Product : BaseEntity<Guid> // Guid IdImplements IAuditable. Fields are automatically stamped by AuditInterceptor.
public abstract class AuditableEntity<TKey> : BaseEntity<TKey>, IAuditable
{
public DateTime CreatedAt { get; set; }
public string? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
}public class Article : AuditableEntity<Guid> { }Implements IAuditable + ISoftDeletable. Deletions are converted to soft deletes by SoftDeleteInterceptor.
public abstract class SoftDeletableEntity<TKey> : AuditableEntity<TKey>, ISoftDeletable
{
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; }
}public class Customer : SoftDeletableEntity { }Implements IAuditable, ISoftDeletable, and IConcurrencyAware.
public abstract class FullEntity<TKey> : SoftDeletableEntity<TKey>, IConcurrencyAware
{
public byte[] RowVersion { get; set; } = [];
}public class Invoice : FullEntity { }Use the configuration base classes to auto-apply standard EF Core mappings without writing them yourself.
public abstract class BaseEntityConfiguration<TEntity, TKey>
: IEntityTypeConfiguration<TEntity> where TEntity : BaseEntity<TKey>
{
public void Configure(EntityTypeBuilder<TEntity> builder)
{
builder.HasKey(e => e.Id);
ConfigureEntity(builder);
}
protected abstract void ConfigureEntity(EntityTypeBuilder<TEntity> builder);
}Adds on top of BaseEntityConfiguration:
- Audit columns configured (no extra code needed)
- Index on
CreatedAt
Adds on top of AuditableEntityConfiguration:
IsDeleteddefault value offalse- Composite index on
(IsDeleted, CreatedAt)
// Shorthand generic aliases for int PK entities
public class CustomerConfiguration : SoftDeletableEntityConfiguration<Customer>
{
protected override void ConfigureEntity(EntityTypeBuilder<Customer> builder)
{
builder.Property(c => c.Name).HasMaxLength(200).IsRequired();
builder.HasIndex(c => c.Email).IsUnique();
}
}Register your configurations in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}Add IFullAuditable to any entity when you want every property change recorded in an AuditLog table:
public class Invoice : AuditableEntity, IFullAuditable { }IFullAuditable extends IAuditable — no extra properties on your entity. You need fullLog: true in your setup and a DbSet<AuditLog> on your context. See Audit Trail for details.
Add IConcurrencyAware to use EF Core row-version optimistic concurrency:
public class Order : AuditableEntity, IConcurrencyAware
{
public byte[] RowVersion { get; set; } = [];
}When a DbUpdateConcurrencyException occurs, EfCoreDbContext automatically wraps it in a ConcurrencyConflictException:
try
{
await context.SaveChangesAsync();
}
catch (ConcurrencyConflictException ex)
{
// ex.EntityType — name of the conflicting entity
// ex.EntityId — primary key of the conflicting row
}