Documentation
how-to/entity-refactoring-guid.md
Pattern: Entity Refactoring to GUID Primary Keys
This pattern documents the step-by-step process for refactoring a component's entities from long/int primary keys to Guid primary keys, following ADR 034.
When to Use This Pattern
Apply this pattern when:
- Refactoring an existing component to use GUID primary keys
- Creating a new component and want to follow current standards
- Converting junction entities to implicit many-to-many relationships
- Standardizing TenantId across components
Prerequisites
- Read ADR 034: GUID Primary Keys
- Understand the component's current entity structure
- Identify which entities are domain entities (get Guids) vs reference data (keep int/short)
- Backup database if refactoring existing data
Step-by-Step Process
Phase 1: Analysis
1. Inventory Entities
List all entity classes in the component and categorize:
Domain Entities (→ Guid):
- User
- Role
- Permission
- Group
- RefreshToken
Reference Data (→ int/short):
- UserStatus
- Tenant (special case - discuss)
Junction Tables (→ Delete):
- UserRole
- RolePermission
2. Identify Passport Entities
Which entities participate in the global Passport registry?
- Usually: Core domain entities with cross-component references
- Example: User, Group in Identity component
3. Map Relationships
Document all foreign key relationships:
User.Id ← RefreshToken.UserId
Role.Id ← UserRole.RoleId (will become implicit)
Permission.Id ← RolePermission.PermissionId (will become implicit)
Phase 2: Delete Junction Entities
For each junction table being replaced with implicit many-to-many:
Delete the entity class file:
rm src/Component.Abstractions/Database/UserRole.csRemove DbSet from DbContext:
// REMOVE: public virtual DbSet<UserRole> UserRoles { get; set; }Remove entity configuration from OnModelCreating:
// REMOVE entire entity configuration block: modelBuilder.Entity<UserRole>(entity => { ... });
Phase 3: Refactor Domain Entities
For each domain entity, apply this template:
BEFORE:
[Table("users")]
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id", Order = 1)]
public long Id { get; set; }
[Column("tenant_id", Order = 34)]
public long TenantId { get; set; }
// Navigation - old junction
[InverseProperty(nameof(UserRole.User))]
public virtual ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
}
AFTER:
[Table("users")]
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Column("id", Order = 1)]
public Guid Id { get; set; }
[Column("platform_type_id", Order = 2)]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public short PlatformTypeId { get; set; } = 30; // PTIDS.USER
[Column("tenant_id", Order = 34)]
public Guid TenantId { get; set; }
// Navigation - implicit many-to-many
public virtual ICollection<Role> Roles { get; set; } = new List<Role>();
}
Checklist for each entity:
- Change
Idtype fromlong/inttoGuid - Change
[DatabaseGenerated]toNone - Add
PlatformTypeIdproperty if entity uses Passports - Change
TenantIdtype toGuid(and make non-nullable if applicable) - Add
TenantIdproperty if missing - Update foreign key property types (e.g.,
UserId→Guid) - Replace junction collection navigation with direct collection (e.g.,
UserRoles→Roles) - Remove
[InverseProperty]attributes on many-to-many navigations
Phase 4: Update DbContext
1. Remove deleted entity DbSets and configurations (done in Phase 2)
2. Add implicit many-to-many configurations:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ... existing configurations ...
// Many-to-many: User <-> Role
modelBuilder.Entity<User>()
.HasMany(u => u.Roles)
.WithMany(r => r.Users)
.UsingEntity(j => j.ToTable("user_roles", "identity"));
// Many-to-many: Role <-> Permission
modelBuilder.Entity<Role>()
.HasMany(r => r.Permissions)
.WithMany(p => p.Roles)
.UsingEntity(j => j.ToTable("role_permissions", "identity"));
}
3. Add Passport configurations (if Prism.Abstractions referenced):
using Acsis.Dynaplex.Engines.Prism.Abstractions;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ... other configurations ...
modelBuilder.Entity<User>().HasPassport(PTIDS.USER);
modelBuilder.Entity<Group>().HasPassport(PTIDS.GROUP);
}
Phase 5: Reference Data Entities
For lookup/reference data entities:
Option A: Keep numeric IDs (recommended for frequently-joined lookups)
[Table("user_statuses")]
public class UserStatus
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id")]
public int Id { get; set; } // Keep as int
[Column("name")]
public string Name { get; set; } = null!;
}
Option B: Optimize long → int/short
// BEFORE: long Id
// AFTER: int Id (if < 2 billion records) or short Id (if < 32k records)
public short Id { get; set; }
Criteria for keeping numeric:
- Small, rarely-changing lookup tables
- Frequently used in joins
- No cross-component references
- Examples: statuses, types, categories
Phase 6: Build and Verify
1. Build the Abstractions project:
dotnet build engines/{component}/src/Acsis.Dynaplex.Engines.{Component}.Abstractions/
2. Check for compilation errors:
- Fix any foreign key type mismatches
- Update any DTO/Response classes that reference entity IDs
- Update any queries that cast IDs
3. Review warnings:
- Nullable reference warnings are acceptable (pre-existing)
- Watch for actual type mismatch warnings
Phase 7: Migration Generation (Future Step)
Note: Don't generate migrations yet! This is a breaking schema change that requires careful data migration planning.
When ready to migrate data:
- Generate EF Core migration
- Review the migration for correctness
- Add custom migration code for data conversion if needed
- Test on non-production database first
Common Patterns
Pattern: Entity with Passport
[Table("users")]
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Column("id")]
public Guid Id { get; set; }
[Column("platform_type_id")]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public short PlatformTypeId { get; set; } = PTIDS.USER;
[Column("tenant_id")]
public Guid TenantId { get; set; }
// No separate passport_id column!
}
Pattern: Entity without Passport
[Table("permissions")]
public class Permission
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Column("id")]
public Guid Id { get; set; }
[Column("tenant_id")]
public Guid TenantId { get; set; }
// No PlatformTypeId needed
}
Pattern: Implicit Many-to-Many
// Entity 1
public class User
{
public Guid Id { get; set; }
public virtual ICollection<Role> Roles { get; set; } = new List<Role>();
}
// Entity 2
public class Role
{
public Guid Id { get; set; }
public virtual ICollection<User> Users { get; set; } = new List<User>();
}
// DbContext
modelBuilder.Entity<User>()
.HasMany(u => u.Roles)
.WithMany(r => r.Users)
.UsingEntity(j => j.ToTable("user_roles", "identity"));
Completed Examples
Identity Component (Reference Implementation)
The Identity component serves as the canonical example:
Files Changed:
- ✅
User.cs- long → Guid, added PlatformTypeId - ✅
Group.cs- long → Guid, added PlatformTypeId - ✅
Role.cs- long → Guid - ✅
Permission.cs- long → Guid, added TenantId - ✅
RefreshToken.cs- UserId long → Guid, added TenantId - ✅
IdentityDb.cs- Updated with implicit many-to-many
Files Deleted:
- ❌
UserRole.cs - ❌
RolePermission.cs - ❌
SecurityQuestion.cs - ❌
SecurityQuestionResponse.cs - ❌
NotificationPreference.cs
Build Result: ✅ Success (0 errors, 19 pre-existing warnings)
Troubleshooting
Issue: "Cannot convert Guid to long"
Cause: Missed updating a foreign key property type
Solution: Find all properties that reference the changed entity's ID and update their type:
// Find:
public long UserId { get; set; }
// Change to:
public Guid UserId { get; set; }
Issue: "The entity type requires a primary key"
Cause: Removed [Key] attribute accidentally
Solution: Ensure [Key] attribute remains on the Id property
Issue: "Navigation property not found"
Cause: Removed navigation property or changed its name
Solution: Update the [InverseProperty] attribute or remove it for many-to-many
Checklist: Component Refactoring
Use this checklist for each component:
Planning:
- Read ADR 001
- Inventory all entities
- Identify domain vs reference data
- Identify Passport entities
- Map all relationships
- Identify junction tables for deletion
Execution:
- Delete junction entity files
- Remove junction DbSets from DbContext
- Remove junction configurations from OnModelCreating
- Refactor each domain entity (ID, PlatformTypeId, TenantId)
- Update all foreign key property types
- Update navigation properties
- Add implicit many-to-many configurations
- Optimize reference data entities (long → int/short)
Validation:
- Build Abstractions project successfully
- Review all compilation warnings
- Verify navigation properties are correct
- Check that TenantId is Guid everywhere
- Confirm Passport entities have PlatformTypeId
Documentation:
- Update component's README if ID types mentioned
- Note any deviations from pattern
- Document any tricky migration steps