Entities & Models
Forge.Repository defines two entity contracts in the Forge.Interfaces namespace. Every entity you want to use with IRepositoryAsync<T> or IUnitOfWork must implement at least one of them.
IBaseModel
The minimum contract. Every Forge entity must have a string Id.
namespace Forge.Interfaces;
public interface IBaseModel
{
string Id { get; set; }
}BaseModel
Implementaion of IBaseModel in Forge.
using Forge.Interfaces;
namespace Forge.Models;
public abstract class BaseModel : IBaseModel
{
public virtual required string Id { get; set; }
}
Why a string Id?
Using string for the identifier gives you full flexibility over your ID strategy without any framework coupling:
| Strategy | Example value | Notes |
|---|---|---|
| ULID | 01HX9Z3BKFGPV6M2K1N8Q7RW5D | Lexicographically sortable, URL-safe, recommended |
| GUID | 550e8400-e29b-41d4-a716-446655440000 | Standard, well-understood |
| NanoId | V1StGXR8_Z5jdHi6B-myT | Compact, URL-safe |
| Sequential int as string | "42" | For legacy schemas |
// ULID (recommended — sortable + compact)
public string Id { get; set; } = Ulid.NewUlid().ToString();
// GUID
public string Id { get; set; } = Guid.NewGuid().ToString();Minimal entity example with your own BaseModel
using Forge.Models;
public abstract class MyBaseModel : BaseModel
{
public override required string Id { get; set; } = Ulid.NewUlid().ToString();
}Minimal entity example
public class Tag : MyBaseModel
{
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
}IAuditableBaseModel
Extends IBaseModel with a full audit trail and soft-delete support. Use this for any entity where you need to know who created or modified a record and when, or where you want logical deletion instead of physical row removal.
namespace Forge.Interfaces;
public interface IAuditableBaseModel : IBaseModel
{
DateTime CreatedOn { get; set; }
DateTime ModifiedOn { get; set; }
string CreatedBy { get; set; }
string ModifiedBy { get; set; }
bool IsDeleted { get; set; }
void OnCreate();
void OnUpdate();
void OnDelete();
}AuditableBaseModel
using Forge.Interfaces;
namespace Forge.Models;
public abstract class AuditableBaseModel : BaseModel, IAuditableBaseModel
{
public virtual DateTime CreatedOn { get; set; }
public virtual DateTime ModifiedOn { get; set; }
public virtual string CreatedBy { get; set; }
public virtual string ModifiedBy { get; set; }
public virtual bool IsDeleted { get; set; }
public virtual void OnCreate()
{
CreatedOn = DateTime.UtcNow;
}
public virtual void OnDelete()
{
IsDeleted = true;
ModifiedOn = DateTime.UtcNow;
}
public virtual void OnUpdate()
{
ModifiedOn = DateTime.UtcNow;
}
}
Properties
| Property | Type | Description |
|---|---|---|
CreatedOn | DateTime | UTC timestamp when the entity was first persisted. Set inside OnCreate(). |
ModifiedOn | DateTime | UTC timestamp updated on every save. Set inside OnUpdate(). |
CreatedBy | string | Identity of the creator — populate from your auth context. |
ModifiedBy | string | Identity of the last modifier — populate from your auth context. |
IsDeleted | bool | Soft-delete flag. When true, the record is logically deleted. Set inside OnDelete(). |
Lifecycle hooks
Forge.Repository’s DbContext base class calls these methods at the appropriate point in the EF Core save pipeline — you never need to call them yourself.
| Hook | Called when | Typical implementation |
|---|---|---|
OnCreate() | EntityState.Added before SaveChangesAsync | Set CreatedOn = DateTime.UtcNow |
OnUpdate() | EntityState.Modified before SaveChangesAsync | Set ModifiedOn = DateTime.UtcNow |
OnDelete() | EntityState.Deleted before SaveChangesAsync | Set IsDeleted = true (soft delete) |
Because OnDelete() sets IsDeleted = true and Forge converts Deleted state to Modified, calling RemoveAsync on an auditable entity never physically deletes the row. It performs a soft delete automatically.
Full implementation example
using Forge.Interfaces;
public class Order : AuditableBaseModel
{
// Business fields
public string CustomerId { get; set; } = string.Empty;
public decimal Total { get; set; }
public OrderStatus Status { get; set; } = OrderStatus.Pending;
// AuditableBaseModel — already have the audit trail fields
// Lifecycle hooks — called automatically by Forge OnCreate(), OnUpdate(), OnDelete()
}Populating CreatedBy / ModifiedBy
The CreatedBy and ModifiedBy fields are not populated automatically - Forge has no knowledge of your authentication layer.
The recommended approach is to override WriteAuditData in your AppDbContext and resolve the current user via IHttpContextAccessor or your preferred identity service.
using Forge.Interfaces;
using Forge.Repository;
using Microsoft.EntityFrameworkCore;
public class AppDbContext(DbContextOptions<AppDbContext> options): DbContext(options)
{
protected override void WriteAuditData()
{
base.WriteAuditData();
// In a your application, you would get the user object from the context of the request or your identity service or whatever way is convenient for you.
string currentUserId = "01HX9Z3BKFGPV6M2K1N8Q7RW5D";
var entries = ChangeTracker.Entries<IAuditableBaseModel>().Where(e => e.State is EntityState.Added or EntityState.Modified);
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedBy = currentUserId;
}
if (entry.State == EntityState.Modified)
{
entry.Entity.ModifiedBy = currentUserId;
}
}
}
protected override void OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder)
{
//Apply all configurations from assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}Filtering Soft-Deleted Records
Since IsDeleted is a plain boolean on your entity, you filter it with standard LINQ predicates or inside a criteria object.
// Direct predicate
var active = await _repo.FindAllAsync(p => !p.IsDeleted, ct);
// Or use a global query filter in your Model builder Configuration in DbContext.
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
// Global query filter to exclude soft-deleted records
builder.HasQueryFilter(x => !x.IsDeleted);
// Base
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).HasMaxLength(36);
// Auditable
builder.Property(x => x.CreatedBy).HasMaxLength(36).IsRequired();
builder.Property(x => x.CreatedOn).IsRequired();
builder.Property(x => x.ModifiedBy).HasMaxLength(36).IsRequired(false);
builder.Property(x => x.ModifiedOn);
builder.Property(x => x.IsDeleted).IsRequired();
// Order
builder.Property(x => x.OrderDate).IsRequired();
builder.Property(x => x.TotalAmount).IsRequired().HasPrecision(18, 4);
// Relationships
builder.HasMany(x => x.OrderProducts)
.WithOne(x => x.Order)
.HasForeignKey(x => x.OrderId)
.OnDelete(DeleteBehavior.NoAction);
}
}If you add a global query filter, all queries through the repository will automatically exclude soft-deleted records. Call .IgnoreQueryFilters() in a raw SQL criteria if you ever need to include them (e.g. admin restore operations).
BaseModel vs AuditableBaseModel — When to Use Which
| Use case | Recommended interface or class |
|---|---|
| Reference / lookup data (tags, categories, enums) | IBaseModel or BaseModel |
| User-generated content, transactional records | IAuditableBaseModel or AuditableBaseModel |
| Any record that requires a deletion log | IAuditableBaseModel or AuditableBaseModel |
| Read-only projections / view entities | IBaseModel or BaseModel |
| Entities mapped to stored procedures | IBaseModel or BaseModel |