entity-development
Use when creating or modifying domain entities with static expressions, computed properties, field tracking, and validation methods.
$ Installieren
git clone https://github.com/duc01226/EasyPlatform /tmp/EasyPlatform && cp -r /tmp/EasyPlatform/.github/skills/backend-entity-development ~/.claude/skills/EasyPlatform// tip: Run this command in your terminal to install the skill
SKILL.md
name: entity-development description: Use when creating or modifying domain entities with static expressions, computed properties, field tracking, and validation methods.
Entity Development Workflow
When to Use This Skill
- Creating new domain entities
- Adding computed properties to entities
- Creating static expression methods (filtering, queries)
- Implementing entity validation
- Setting up field change tracking
Pre-Flight Checklist
- Identify the correct service/domain
- Check for similar entities:
grep "class {EntityName}" --include="*.cs" - Identify if audited or non-audited entity needed
- Check existing expression patterns in domain
File Location
{Service}.Domain/
âââ Entities/
âââ {EntityName}.cs
Entity Type Decision
Non-Audited Entity (Basic)
public class Employee : RootEntity<Employee, string>
{
// No CreatedBy, UpdatedBy, CreatedDate, etc.
}
Audited Entity (With Audit Trail)
public class AuditedEmployee : RootAuditedEntity<AuditedEmployee, string, string>
{
// Includes: CreatedBy, UpdatedBy, CreatedDate, UpdatedDate
}
Implementation Pattern
Basic Entity Structure
[TrackFieldUpdatedDomainEvent] // Optional: Track all field changes
public sealed class Entity : RootEntity<Entity, string>
{
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// CORE PROPERTIES
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[TrackFieldUpdatedDomainEvent] // Track specific field changes
public string Name { get; set; } = "";
public string Code { get; set; } = "";
public string CompanyId { get; set; } = "";
public EntityStatus Status { get; set; }
public DateTime? EffectiveDate { get; set; }
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// NAVIGATION PROPERTIES
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[JsonIgnore]
public Company? Company { get; set; }
[JsonIgnore]
public List<EntityChild>? Children { get; set; }
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// COMPUTED PROPERTIES (MUST have empty set { })
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[ComputedEntityProperty]
public bool IsActive
{
get => Status == EntityStatus.Active && !IsDeleted;
set { } // Required empty setter for EF Core
}
[ComputedEntityProperty]
public string DisplayName
{
get => $"{Code} - {Name}".Trim();
set { } // Required empty setter
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// STATIC EXPRESSIONS (For Repository Queries)
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// Simple filter expression
public static Expression<Func<Entity, bool>> OfCompanyExpr(string companyId)
=> e => e.CompanyId == companyId;
// Unique constraint expression
public static Expression<Func<Entity, bool>> UniqueExpr(string companyId, string code)
=> e => e.CompanyId == companyId && e.Code == code;
// Filter by status list
public static Expression<Func<Entity, bool>> FilterByStatusExpr(List<EntityStatus> statuses)
{
var statusSet = statuses.ToHashSet();
return e => e.Status.HasValue && statusSet.Contains(e.Status.Value);
}
// Composite expression with conditional
public static Expression<Func<Entity, bool>> ActiveInCompanyExpr(string companyId, bool includeInactive = false)
=> OfCompanyExpr(companyId).AndAlsoIf(!includeInactive, () => e => e.IsActive);
// Full-text search columns
public static Expression<Func<Entity, object?>>[] DefaultFullTextSearchColumns()
=> [e => e.Name, e => e.Code, e => e.Description];
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ASYNC EXPRESSIONS (When External Dependencies Needed)
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
public static async Task<Expression<Func<Entity, bool>>> FilterWithLicenseExprAsync(
IRepository<License> licenseRepo,
string companyId,
CancellationToken ct = default)
{
var hasLicense = await licenseRepo.HasLicenseAsync(companyId, ct);
return hasLicense ? PremiumFilterExpr() : StandardFilterExpr();
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// VALIDATION METHODS
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// Naming Convention:
// - Validate[Context]() â Returns PlatformValidationResult, never throws
// - Ensure[Context]Valid() â Returns void/T, throws PlatformValidationException
// - At call site: Use Validate...().EnsureValid() instead of wrapper Ensure methods
public PlatformValidationResult ValidateCanBeUpdated()
{
return PlatformValidationResult.Valid()
.And(() => !IsDeleted, "Entity is deleted")
.And(() => Status != EntityStatus.Locked, "Entity is locked");
}
public async Task<PlatformValidationResult> ValidateAsync(
IRepository<Entity> repository,
CancellationToken ct = default)
{
return await PlatformValidationResult.Valid()
.And(() => Name.IsNotNullOrEmpty(), "Name is required")
.And(() => Code.IsNotNullOrEmpty(), "Code is required")
.AndNotAsync(async () => await repository.AnyAsync(
e => e.Id != Id && e.CompanyId == CompanyId && e.Code == Code, ct),
"Code already exists");
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// INSTANCE METHODS
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
public void Activate() => Status = EntityStatus.Active;
public void Deactivate() => Status = EntityStatus.Inactive;
public void Reset() { /* reset logic */ }
}
Expression Composition Patterns
| Pattern | Usage | Example |
|---|---|---|
AndAlso | Combine two expressions with AND | expr1.AndAlso(expr2) |
OrElse | Combine two expressions with OR | expr1.OrElse(expr2) |
AndAlsoIf | Conditional AND | .AndAlsoIf(condition, () => expr) |
OrElseIf | Conditional OR | .OrElseIf(condition, () => expr) |
// Composing multiple expressions
var expr = Entity.OfCompanyExpr(companyId)
.AndAlso(Entity.FilterByStatusExpr(statuses))
.AndAlsoIf(deptIds.Any(), () => Entity.FilterByDeptExpr(deptIds))
.AndAlsoIf(searchText.IsNotNullOrEmpty(), () => Entity.SearchExpr(searchText));
Computed Property Rules
:white_check_mark: MUST have empty setter set { }
[ComputedEntityProperty]
public bool IsRoot
{
get => Id == RootId;
set { } // Required for EF Core mapping
}
:x: Wrong - No setter
[ComputedEntityProperty]
public bool IsRoot => Id == RootId; // Will cause EF Core issues
Anti-Patterns to AVOID
:x: Business logic in properties
// WRONG - calling services in property
public bool IsValid => validationService.Validate(this);
:x: Missing [JsonIgnore] on navigation
// WRONG - circular reference issues
public Company Company { get; set; }
:x: Complex logic in getters
// WRONG - should be a method
public List<Item> ActiveItems => Items.Where(x => x.IsActive).ToList();
Verification Checklist
- Entity inherits from correct base class (RootEntity or RootAuditedEntity)
- Computed properties have
[ComputedEntityProperty]and emptyset { } - Navigation properties have
[JsonIgnore] - Static expressions follow naming convention:
{Purpose}Expr - Full-text search columns defined if searchable
- Validation methods return
PlatformValidationResult -
[TrackFieldUpdatedDomainEvent]on tracked fields
Repository

duc01226
Author
duc01226/EasyPlatform/.github/skills/backend-entity-development
2
Stars
0
Forks
Updated4d ago
Added1w ago