Troubleshooting foreign key relationships in EF Core with strongly typed IDs in DDD

93 Views Asked by At

I'm currently diving into Domain-Driven Design (DDD) and putting it into practice with EF Core. In my domain model, I've opted for strongly typed identifiers. To accommodate this in EF Core, I've utilized domain type conversion to map these identifiers to Guid types, which are supported by EF Core. However, I've encountered challenges when attempting to establish relationships between entities.

When configuring relationships in EF Core, I've encountered an issue where EF Core recognizes the property designated to hold the foreign key as a different type and consequently creates a shadow property.

The error message reads:

The foreign key property 'Communities.UserId1' was created in shadow state because a conflicting property with the simple name 'UserId' exists in the entity type, but is either not mapped, is already used for another relationship, or is incompatible with the associated primary key type.

Furthermore, in my attempt to resolve this issue, I refrained from converting the property, only to encounter another error:

The 'UserId' property 'Communities.UserId' could not be mapped because the database provider does not support this type.

It's worth noting that if I refrain from establishing any relationships, the migration process proceeds smoothly. However, this results in a lack of relationships, leading to inconsistencies within the properties.

Could you please provide guidance on resolving these issues and configuring EF Core to establish relationships correctly while accommodating strongly typed identifiers within a DDD context?

Below is an example of the context of my domain

There is my community Domain

 public sealed class Community
    : AggregateRoot<CommunityId, Guid>
{
    public UserId UserId { get; private set; }
    public string Name { get; private set; }
    public string Description { get; private set; }
    public string Topic { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime UpdatedAt { get; private set; }
}

There is aggregate root

    public abstract class AggregateRoot<TId, TIdType> : Entity<TId> where TId : AggregateRootId<TIdType>
{
    public new AggregateRootId<TIdType> Id { get; protected set; }

    protected AggregateRoot(TId id)
    {
        Id = id;
    }

    protected AggregateRoot(){}

}

There is AggregaterootId

public abstract class AggregateRootId<TId> : ValueObject
{
    public abstract TId Value { get; protected set; }
}

There is Entity

    public abstract class Entity<TId> : IEquatable<Entity<TId>>
where TId : notnull
{
    public TId Id { get; protected set; }

    public Entity() { }

    protected Entity(TId id)
    {
        Id = id;
    }

    public override bool Equals(object? obj)
    {
        return obj is Entity<TId> entity && Id.Equals(entity.Id);
    }

    public bool Equals(Entity<TId>? other)
    {
        return Equals((object?)other);
    }

    public static bool operator ==(Entity<TId> left, Entity<TId> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Entity<TId> left, Entity<TId> right)
    {
        return !Equals(left, right);
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

There is Valueobject

    public abstract class ValueObject: IEquatable<ValueObject>
{
    public abstract IEnumerable<object> GetEqualityComponents();
    public override bool Equals(object? obj)
    {
        if(obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var valueObject = (ValueObject)obj;

        return GetEqualityComponents()
            .SequenceEqual(valueObject.GetEqualityComponents());
    }

    public static bool operator ==(ValueObject left, ValueObject right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ValueObject left, ValueObject right)
    {
        return !Equals(left, right);
    }

    public override int GetHashCode()
    {
       return GetEqualityComponents()
        .Select(x => x?.GetHashCode() ?? 0)
        .Aggregate((x, y) => x ^ y);
    }

    public bool Equals(ValueObject? other)
    {
        return Equals((object?)other);
    }
}

There is userId type

    public sealed class UserId : AggregateRootId<Guid>
{
    public override Guid Value { get; protected set; }

    public UserId(Guid value)
    {
        Value = value;
    }

    public static UserId CreateUnique()
    {
        return new(Guid.NewGuid());
    }

    public static UserId Create(Guid guid)
    {
        return new UserId(guid);
    }

    public override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}

Last but not least

My EfCore Community config

 public class CommunityConfiguration
 : IEntityTypeConfiguration<Community>
{
    public void Configure(EntityTypeBuilder<Community> builder)
    {
        ConfigureCommunityTable(builder);
    }

    private void ConfigureCommunityTable(EntityTypeBuilder<Community> builder)
    {
        builder.ToTable("Communities");

        builder.HasKey(c => c.Id);

        builder.Property(c => c.Id)
             .ValueGeneratedNever()
             .HasColumnName("Id")
             .HasConversion(id => id.Value,
                 value => CommunityId.Create(value));

        builder.Property(c => c.UserId)
             .ValueGeneratedNever()
             .HasColumnName("UserId")
             .HasConversion(id => id.Value,
                 value => UserId.Create(value));

        builder.Property(c => c.Name)
            .HasMaxLength(100);

        builder.Property(c => c.Description)
            .HasMaxLength(200);

        builder.Property(c => c.Topic)
            .HasMaxLength(100);

        builder.Property(c => c.CreatedAt);

        builder.Property(c => c.UpdatedAt);
    }
}

I've tried a loot of configurations in there but that on is the without relationship.

1

There are 1 best solutions below

0
Mateus Zampol On

I tried tweaking your code and this is a working solution:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();

serviceCollection.AddDbContext<MyDb>(o =>
{
    o.UseInMemoryDatabase("DDD");
});

var services = serviceCollection.BuildServiceProvider();

var db = services.GetRequiredService<MyDb>();

var stackOverflow = new Community("StackOverflow");
var marco = new User("Marco");

stackOverflow.Users.Add(marco);

marco.Community = stackOverflow;

db.Communities.Add(stackOverflow);

db.SaveChanges();

foreach (var community in db.Communities)
{
    Console.WriteLine($"Members of {community.Name}:");
    foreach (var user in community.Users)
    {
        Console.WriteLine(user.Name);
    }
}

class MyDb : DbContext
{
    public MyDb()
    {
    }

    public MyDb(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Community>(builder =>
        {
            builder.ToTable("Communities");

            builder.HasKey(c => c.Id);

            builder.Property(c => c.Id)
                 .ValueGeneratedNever()
                 .HasConversion(id => id.Value,
                     value => CommunityId.Create(value));

            builder.Property(c => c.Name)
                .HasMaxLength(100);

            builder.HasMany(c => c.Users)
                .WithOne(u => u.Community)
                .HasForeignKey("CommunityId");

        });

        modelBuilder.Entity<User>(builder =>
        {
            builder.ToTable("Users");

            builder.HasKey(u => u.Id);

            builder.Property(u => u.Name)
                .HasMaxLength(100);

            builder.Property(u => u.Id)
                 .ValueGeneratedNever()
                 .HasConversion(id => id.Value,
                     value => UserId.Create(value));

            builder.Property<CommunityId>("CommunityId")
                .HasConversion(x => x.Value, g => CommunityId.Create(g));
        });
    }
    public virtual DbSet<Community> Communities { get; set; }
}

public class Community
   : AggregateRoot<CommunityId, Guid>
{
    public Community(string name)
    {
        Id = CommunityId.CreateUnique();
        Name = name;
    }
    public string Name { get; private set; }
    public virtual ICollection<User> Users { get; set; } = new HashSet<User>();
}

public class User : Entity<UserId>
{
    public User(string name)
    {
        Id = UserId.CreateUnique();
        Name = name;
    }
    public string Name { get; set; }
    public virtual Community Community { get; set; } = null!;
}
public abstract class AggregateRoot<TId, TIdType> : Entity<TId> where TId : AggregateRootId<TIdType>
{
    protected AggregateRoot(TId id)
    {
        Id = id;
    }

    protected AggregateRoot() { }

}

public abstract class AggregateRootId<TId> : ValueObject
{
    public abstract TId Value { get; protected set; }
}

public abstract class Entity<TId> : IEquatable<Entity<TId>>
where TId : notnull
{
    public TId Id { get; protected set; }

    public Entity() { }

    protected Entity(TId id)
    {
        Id = id;
    }

    public override bool Equals(object? obj)
    {
        return obj is Entity<TId> entity && Id.Equals(entity.Id);
    }

    public bool Equals(Entity<TId>? other)
    {
        return Equals((object?)other);
    }

    public static bool operator ==(Entity<TId> left, Entity<TId> right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Entity<TId> left, Entity<TId> right)
    {
        return !Equals(left, right);
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }
}

public abstract class ValueObject : IEquatable<ValueObject>
{
    public abstract IEnumerable<object> GetEqualityComponents();
    public override bool Equals(object? obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var valueObject = (ValueObject)obj;

        return GetEqualityComponents()
            .SequenceEqual(valueObject.GetEqualityComponents());
    }

    public static bool operator ==(ValueObject left, ValueObject right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ValueObject left, ValueObject right)
    {
        return !Equals(left, right);
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
         .Select(x => x?.GetHashCode() ?? 0)
         .Aggregate((x, y) => x ^ y);
    }

    public bool Equals(ValueObject? other)
    {
        return Equals((object?)other);
    }
}
public sealed class UserId : AggregateRootId<Guid>
{
    public override Guid Value { get; protected set; }

    public UserId(Guid value)
    {
        Value = value;
    }

    public static UserId CreateUnique()
    {
        return new(Guid.NewGuid());
    }

    public static UserId Create(Guid guid)
    {
        return new UserId(guid);
    }

    public override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}
public sealed class CommunityId : AggregateRootId<Guid>
{
    public override Guid Value { get; protected set; }

    public CommunityId(Guid value)
    {
        Value = value;
    }

    public static CommunityId CreateUnique()
    {
        return new(Guid.NewGuid());
    }

    public static CommunityId Create(Guid guid)
    {
        return new CommunityId(guid);
    }

    public override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}

Please note that I used a shadow property for the FK.

The main problem I found was that EF Core was not happy with the public new TId Id in AggregateRoot, and was creating a duplicate shadow property that was never set, so I just removed that (redundant) definition. Hope it helps!