Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strongly Typed Ids in Entity Framework Core

I'm trying to have a strongly typed Id class, which now holds 'long' internally. Implementation below. The problem I'm having the using this in my entities is that Entity Framework gives me a message that the property Id is already mapped onto it. See my IEntityTypeConfiguration below.

Note: I am not aiming to have a rigid DDD implementation. So please keep this in mind when commenting or answering. The whole id behind the typed Id is for developers coming to the project they're strongly typed to use Id in all of their entities, of course translated to long (or BIGINT) - but it is clear then for others.

Below the class & configuration, which doesn't work. The repo can be found at https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31,

  • Id class at (commented out now): https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31/blob/master/Source/Common/Kf.CANetCore31/DomainDrivenDesign/Id.cs
  • Entity and ValueObject classes (where for an Entity the property Id was of the type Id.cs (above): https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31/tree/master/Source/Common/Kf.CANetCore31/DomainDrivenDesign
  • Configurations at: https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31/tree/master/Source/Infrastructure/Persistence/Kf.CANetCore31.Infrastructure.Persistence.Ef/EntityTypeConfigurations

Id class implementation (marked obsolete now, because I abandoned the idea until I found a solution for this)

namespace Kf.CANetCore31.DomainDrivenDesign
{
    [DebuggerDisplay("{DebuggerDisplayString,nq}")]
    [Obsolete]
    public sealed class Id : ValueObject
    {
        public static implicit operator Id(long value)
            => new Id(value);
        public static implicit operator long(Id value)
            => value.Value;
        public static implicit operator Id(ulong value)
            => new Id((long)value);
        public static implicit operator ulong(Id value)
            => (ulong)value.Value;
        public static implicit operator Id(int value)
            => new Id(value);


        public static Id Empty
            => new Id();

        public static Id Create(long value)
            => new Id(value);

        private Id(long id)
            => Value = id;
        private Id()
            : this(0)
        { }

        public long Value { get; }

        public override string DebuggerDisplayString
            => this.CreateDebugString(x => x.Value);

        public override string ToString()
            => DebuggerDisplayString;

        protected override IEnumerable<object> EquatableValues
            => new object[] { Value };
    }
}

EntityTypeConfiguration I was using when Id not marked obsolete for entity Person Unfortunately though, when of type Id, EfCore didn't want to map it... when of type long it was no problem... Other owned types, as you see (with Name) work fine.

public sealed class PersonEntityTypeConfiguration
        : IEntityTypeConfiguration<Person>
    {
        public void Configure(EntityTypeBuilder<Person> builder)
        {
            // this would be wrapped in either a base class or an extenion method on
            // EntityTypeBuilder<TEntity> where TEntity : Entity
            // to not repeated the code over each EntityTypeConfiguration
            // but expanded here for clarity
            builder
                .HasKey(e => e.Id);
            builder
                .OwnsOne(
                e => e.Id,
                id => {
                   id.Property(e => e.Id)
                     .HasColumnName("firstName")
                     .UseIdentityColumn(1, 1)
                     .HasColumnType(SqlServerColumnTypes.Int64_BIGINT);
                }

            builder.OwnsOne(
                e => e.Name,
                name =>
                {
                    name.Property(p => p.FirstName)
                        .HasColumnName("firstName")
                        .HasMaxLength(150);
                    name.Property(p => p.LastName)
                        .HasColumnName("lastName")
                        .HasMaxLength(150);
                }
            );

            builder.Ignore(e => e.Number);
        }
    }

Entity base class (when I was still using Id, so when it wasn't marked obsolete)

namespace Kf.CANetCore31.DomainDrivenDesign
{
    /// <summary>
    /// Defines an entity.
    /// </summary>
    [DebuggerDisplay("{DebuggerDisplayString,nq}")]
    public abstract class Entity
        : IDebuggerDisplayString,
          IEquatable<Entity>
    {
        public static bool operator ==(Entity a, Entity b)
        {
            if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
                return true;

            if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
                return false;

            return a.Equals(b);
        }

        public static bool operator !=(Entity a, Entity b)
            => !(a == b);

        protected Entity(Id id)
            => Id = id;

        public Id Id { get; }

        public override bool Equals(object @object)
        {
            if (@object == null) return false;
            if (@object is Entity entity) return Equals(entity);
            return false;
        }

        public bool Equals(Entity other)
        {
            if (other == null) return false;
            if (ReferenceEquals(this, other)) return true;
            if (GetType() != other.GetType()) return false;
            return Id == other.Id;
        }

        public override int GetHashCode()
            => $"{GetType()}{Id}".GetHashCode();

        public virtual string DebuggerDisplayString
            => this.CreateDebugString(x => x.Id);

        public override string ToString()
            => DebuggerDisplayString;
    }
}

Person (the domain and references to the other ValueObjects can be found at https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31/tree/master/Source/Core/Domain/Kf.CANetCore31.Core.Domain/People)

namespace Kf.CANetCore31.Core.Domain.People
{
    [DebuggerDisplay("{DebuggerDisplayString,nq}")]
    public sealed class Person : Entity
    {
        public static Person Empty
            => new Person();

        public static Person Create(Name name)
            => new Person(name);

        public static Person Create(Id id, Name name)
            => new Person(id, name);

        private Person(Id id, Name name)
            : base(id)
            => Name = name;
        private Person(Name name)
            : this(Id.Empty, name)
        { }
        private Person()
            : this(Name.Empty)
        { }

        public Number Number
            => Number.For(this);
        public Name Name { get; }

        public override string DebuggerDisplayString
            => this.CreateDebugString(x => x.Number.Value, x => x.Name);
    }
}
like image 683
Yves Schelpe Avatar asked Feb 10 '20 16:02

Yves Schelpe


2 Answers

So after searching a long while, and trying to get some more answer, I found it, here it is then. Thanks to Andrew Lock.

Strongly-typed IDs in EF Core: Using strongly-typed entity IDs to avoid primitive obsession - Part 4: https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-4/

TL;DR / Summary of Andrew In this post I describe a solution to using strongly-typed IDs in your EF Core entities by using value converters and a custom IValueConverterSelector. The base ValueConverterSelector in the EF Core framework is used to register all built-in value conversions between primitive types. By deriving from this class, we can add our strongly-typed ID converters to this list, and get seamless conversion throughout our EF Core queries

like image 65
Yves Schelpe Avatar answered Sep 27 '22 22:09

Yves Schelpe


I think you are out of luck. Your use case is extremely rare. And EF Core 3.1.1 is still struggling with putting SQL onto the database that is not broken in anything except the most base cases.

So, you would have to write something that goes through the LINQ tree and this likely is a tremendous amount of work, and if you stumble onto bugs on EF Core - which you will - have fun explaining that in your tickets.

like image 35
TomTom Avatar answered Sep 28 '22 00:09

TomTom