Documentation Index
Fetch the complete documentation index at: https://docs.aspfox.com/llms.txt
Use this file to discover all available pages before exploring further.
A condensed reference for adding a new EF Core entity. For the full walkthrough including commands, queries, and frontend, see Adding a Feature.
Checklist
Create the entity class in Domain
// src/Acme.Domain/Entities/Tag.cs
public class Tag : BaseEntity, ITenantScopedEntity, IAuditableEntity
{
public string Name { get; private set; } = string.Empty;
public string Color { get; private set; } = "#6B7280";
public Guid TenantId { get; private set; }
public Tenant Tenant { get; private set; } = null!;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
private Tag() { }
public static Tag Create(string name, string color, Guid tenantId)
{
return new Tag { Id = Guid.NewGuid(), Name = name, Color = color, TenantId = tenantId };
}
}
Interfaces to implement:
BaseEntity — provides Id (Guid)
ITenantScopedEntity — requires TenantId property; enables global query filter
IAuditableEntity — requires CreatedAt and UpdatedAt; automatically set by SaveChangesAsync
Not tenant-scoped? If the entity belongs to a user rather than a tenant (e.g., user profile data), implement IUserScopedEntity instead and filter by UserId.Create the EF Core configuration
// src/Acme.Infrastructure/Persistence/Configurations/TagConfiguration.cs
public class TagConfiguration : IEntityTypeConfiguration<Tag>
{
private readonly TenantContext _tenantContext;
public TagConfiguration(TenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public void Configure(EntityTypeBuilder<Tag> builder)
{
builder.ToTable("tags");
builder.HasKey(t => t.Id);
builder.Property(t => t.Name).HasMaxLength(100).IsRequired();
builder.Property(t => t.Color).HasMaxLength(7).IsRequired(); // #RRGGBB
builder.HasIndex(t => t.TenantId);
builder.HasIndex(t => new { t.TenantId, t.Name }).IsUnique();
builder.HasOne(t => t.Tenant)
.WithMany()
.HasForeignKey(t => t.TenantId)
.OnDelete(DeleteBehavior.Cascade);
// Required for tenant isolation. Do not skip this.
builder.HasQueryFilter(t => t.TenantId == _tenantContext.CurrentTenantId);
}
}
Register the configuration in ApplicationDbContext
// src/Acme.Infrastructure/Persistence/ApplicationDbContext.cs
// Add DbSet:
public DbSet<Tag> Tags => Set<Tag>();
// In OnModelCreating, add:
modelBuilder.ApplyConfiguration(new TagConfiguration(_tenantContext));
Add to IApplicationDbContext interface
// src/Acme.Application/Common/Interfaces/IApplicationDbContext.cs
DbSet<Tag> Tags { get; }
Create and apply the migration
make shell-api
dotnet ef migrations add AddTagsTable \
--project src/Acme.Infrastructure \
--startup-project src/Acme.Api
exit
make migrate
Verify the migration was applied:make shell-db
\dt
# tags table should appear
\q
Common mistakes
Forgetting the global query filter — if you omit HasQueryFilter, queries return records from all tenants. No error occurs; data leaks silently. Always add the filter for tenant-scoped entities.
Forgetting to register the configuration — if you create TagConfiguration but do not call ApplyConfiguration in OnModelCreating, the entity uses EF Core’s conventions and has no filter, no unique index, and no cascade delete.
Creating the migration before adding DbSet — EF Core discovers entities through DbSet properties. If DbSet<Tag> is missing, the migration will not include the table. Add the DbSet first.
Using int for primary keys — AspFox uses Guid PKs throughout. Using int breaks the BaseEntity contract and makes the admin panel and audit log behave unexpectedly.