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.
I tried tweaking your code and this is a working solution:
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 IdinAggregateRoot, and was creating a duplicate shadow property that was never set, so I just removed that (redundant) definition. Hope it helps!