Search code examples
c#entity-framework-coreforeign-keysdomain-driven-designvalue-objects

Entity Framework Core foreign key relationship Guid to ValueObject type


Problem / Context

In my application, I have two entities where the Id of the entities as represented by the database are GUIDs. However, in code I represent these IDs as

readonly record struct ProjectId(Guid Value)

and

readonly record struct ProjectTaskId(Guid Value)

The intent of this is that within my system and front end these GUIDs are prefixed with project_{guid} and task_{guid} this way when someone calls my API I know what the actual type is and can enforce it throughout the application.

When trying to run

dotnet ef migration <migrationName>

for this project, it fails because EF Core sees the value of the foreign key relation ship between Project and ProjectTask as a Guid and ProjectId respectively.

The specific error that I get is the following:

The exception 'The types of the properties specified for the foreign key {'AssignedProjectId' : ProjectId} on entity type 'ProjectTask' do not match the types of the properties in the principal key {'Id' : Guid} on entity type 'Project'

I have the relationship defined as follows:

public class TenantIdValueConverter : ValueConverter<TenantId, Guid>
{
    public TenantIdValueConverter()
        : base(
            v => v.Value,
            v => new TenantId(v))
    {
    }
}

/** in the DbContext **/

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
        configurationBuilder
           .Properties<ProjectId>()
           .HaveConversion<ProjectIdValueConverter>();
        configurationBuilder
           .Properties<ProjectTaskId>()
           .HaveConversion<ProjectTaskIdValueConverter>();
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
        modelBuilder.Entity<Project>()
           .HasMany(p => p.ProjectTasks)
           .WithOne(pt => pt.Project)
           .HasForeignKey(pt => pt.AssignedProjectId)
           .HasPrincipalKey(p => p.Id)
           .IsRequired()
           .OnDelete(DeleteBehavior.Cascade);
}
public abstract class Entity
{
    public Guid Id { get; }
}

public abstract class AggregateRoot : Entity
{
    protected AggregateRoot(Guid id) : base(id)
    {
    }
}

public class Project : AggregateRoot
{
    public HashSet<ProjectTask> ProjectTasks { get; private set; } = [];
}

public class ProjectTask : Entity
{
    public Project Project { get; set; } = null!;
    public ProjectId AssignedProjectId { get; set; }
}

Effectively Entity Framework Core doesn't know how to handle this relationship since on one side I have a Guid and on the other, I have a ProjectId. Even though I have these mapped to a Guid using a value converter, it doesn't appear that they are honored for a foreign key.

Research and Similar other Problems

I found this other question that seems related though between an int and a long type. This issue isn't quite the same but they are similar.

Options

So far the only option I have been able to find seems to be to convert my Entity/AggrigateRoot abstract classes to allow for a generic primary key so that the type can be ProjectId, ProjectTaskId, and or any of the other Id types that I have. This would be quite a pain to upgrade so I'm trying to avoid it at the moment but it does seem like a valid option.

The other option is to not use a ValueObject like I'm doing in the projects where they are foreign keys but that feels like a bit of a hack and not a good decision to make long term.

Question

Does anyone know of a way to handle the conversion between a ValueObject similar to what I described and an Id of type Guid (or technically any other type)? Does anyone know of a better way of doing the pattern that I'm trying to do with Entity Framework Core? As perhaps I'm just going about this wrong.


Solution

  • The error is due to ProjectId and Guid being different data types. One is a value object with an internal property that is a Guid and the other is a Guid.

    Additionally, that TenantIdValueConverter won't get involved as you're dealing with a ProjectId. Not that a ProjectIdValueConverter would help you here. The ValueConverter is used when mapping to/from the database.

    That 'timesaver' of putting the Id in the base class is a gotcha. You'd need to go for:

    class Project 
    {
      public ProjectId Id { get; set; }
    }
    
    class ProjectTask
    {
      public ProjectTaskId Id { get; set; }
      public ProjectId AssignedProjectId { get; set; }
    }
    

    and so on.

    Using strongly typed ids is a good practice, in my opinion. But I'd recommend reading this blog as a primer on using strongly typed ids:

    https://andrewlock.net/using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-1/

    I wouldn't go too far down the rabbit-hole on that blog and just stick with the first 3 parts and use what you learn there.