Skip to ContentSkip to Content
Entities & Models

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:

StrategyExample valueNotes
ULID01HX9Z3BKFGPV6M2K1N8Q7RW5DLexicographically sortable, URL-safe, recommended
GUID550e8400-e29b-41d4-a716-446655440000Standard, well-understood
NanoIdV1StGXR8_Z5jdHi6B-myTCompact, 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

PropertyTypeDescription
CreatedOnDateTimeUTC timestamp when the entity was first persisted. Set inside OnCreate().
ModifiedOnDateTimeUTC timestamp updated on every save. Set inside OnUpdate().
CreatedBystringIdentity of the creator — populate from your auth context.
ModifiedBystringIdentity of the last modifier — populate from your auth context.
IsDeletedboolSoft-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.

HookCalled whenTypical implementation
OnCreate()EntityState.Added before SaveChangesAsyncSet CreatedOn = DateTime.UtcNow
OnUpdate()EntityState.Modified before SaveChangesAsyncSet ModifiedOn = DateTime.UtcNow
OnDelete()EntityState.Deleted before SaveChangesAsyncSet 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 caseRecommended interface or class
Reference / lookup data (tags, categories, enums)IBaseModel or BaseModel
User-generated content, transactional recordsIAuditableBaseModel or AuditableBaseModel
Any record that requires a deletion logIAuditableBaseModel or AuditableBaseModel
Read-only projections / view entitiesIBaseModel or BaseModel
Entities mapped to stored proceduresIBaseModel or BaseModel