Ids fuertemente tipados en Entity Framework Core

12

Estoy tratando de tener una Idclase fuertemente tipada , que ahora tiene "largo" internamente. Implementación a continuación. El problema que tengo al usar esto en mis entidades es que Entity Framework me da un mensaje de que el ID de la propiedad ya está asignado. Ver mi a IEntityTypeConfigurationcontinuación.

Nota: No pretendo tener una implementación DDD rígida. Por lo tanto, tenga esto en cuenta al comentar o responder . Toda la identificación detrás de lo escrito Ides para los desarrolladores que vienen al proyecto, están fuertemente escritos para usar Id en todas sus entidades, por supuesto traducido a long(o BIGINT), pero está claro para otros.

Debajo de la clase y configuración, que no funciona. El repositorio se puede encontrar en https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31 ,

Idimplementación de clase (marcada como obsoleta ahora, porque abandoné la idea hasta que encontré una solución para esto)

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 };
    }
}

EntityTypeConfigurationEstaba usando cuando Id no marcado como obsoleto para la entidad.Person Desafortunadamente, cuando era de tipo Id, EfCore no quería mapearlo ... cuando era de tipo largo no era problema ... Otros tipos de propiedad, como puede ver (con Name) trabaja bien.

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 clase base (cuando todavía estaba usando Id, entonces cuando no estaba marcado como obsoleto)

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(el dominio y las referencias a los otros ValueObjects se pueden encontrar en 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);
    }
}
Yves Schelpe
fuente

Respuestas:

3

No pretendo tener una implementación rígida de DDD. Por lo tanto, tenga esto en cuenta al comentar o responder. Toda la identificación detrás de la identificación escrita es para los desarrolladores que vienen al proyecto, están fuertemente escritos para usar la identificación en todas sus entidades

Entonces, ¿por qué no simplemente agregar un alias de tipo:

using Id = System.Int64;
David Browne - Microsoft
fuente
Claro, me gusta la idea. Pero cada vez que usará el "Id" en un archivo .cs, ¿no debería asegurarse de colocar esta declaración de uso allí en la parte superior, mientras que con una clase que se transmite, uno no tiene que hacerlo? También perdería otra funcionalidad de clase base como Id.Empty..., o tendría que implementarlo de otra manera en un método de extensión entonces ... Me gusta la idea, gracias por pensar. Si no surge ninguna otra solución, me conformaría con esto, ya que esto indica claramente la intención.
Yves Schelpe
3

Entonces, después de buscar mucho tiempo, y tratando de obtener una respuesta más, la encontré, aquí está entonces. Gracias a Andrew Lock.

ID fuertemente tipados en EF Core: uso de ID de entidad fuertemente tipados para evitar la obsesión primitiva - Parte 4 : https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity- ids-to-evitar-primitive-obsession-part-4 /

TL; DR / Resumen de Andrew En esta publicación describo una solución para usar ID fuertemente tipados en sus entidades EF Core mediante el uso de convertidores de valor y un IValueConverterSelector personalizado. El ValueConverterSelector base en el marco de EF Core se usa para registrar todas las conversiones de valor incorporadas entre tipos primitivos. Al derivar de esta clase, podemos agregar nuestros convertidores de ID fuertemente tipados a esta lista y obtener una conversión perfecta en todas nuestras consultas de EF Core

Yves Schelpe
fuente
2

Creo que no tienes suerte. Su caso de uso es extremadamente raro. Y EF Core 3.1.1 todavía está luchando para poner SQL en la base de datos que no está roto en nada, excepto en la mayoría de los casos base.

Por lo tanto, tendrías que escribir algo que pase por el árbol LINQ y esto probablemente sea una gran cantidad de trabajo, y si te topas con errores en EF Core, lo cual harás, te divertirás explicando eso en tus tickets.

TomTom
fuente
Estoy de acuerdo en que el caso de uso es raro, pero la idea detrás de esto no es del todo estúpida, espero ...? Si es así, por favor hágamelo saber. Si es estúpido (hasta ahora convencido de que no, ya que los identificadores fuertemente tipados son tan fáciles de programar en el dominio), o si no encuentro una respuesta rápidamente, podría usar un alias como lo sugiere David Browne - Micrososft a continuación ( stackoverflow .com / a / 60155275/1155847 ). Hasta ahora todo bien en otros casos de uso, y colecciones y campos ocultos en EF Core, sin errores, por lo que pensé que era extraño, ya que de lo contrario tengo una buena experiencia sólida con el producto.
Yves Schelpe
No es estúpido per se, pero es raro que NINGUNA forma que haya visto lo soporte y EfCore es tan malo que en este momento estoy trabajando para eliminarlo y volver a Ef (no núcleo) porque necesito enviarlo. Para mí, EfCore 2.2 funcionó mejor: 3.1 es 100% inutilizable ya que cualquier proyección que use da como resultado un sql incorrecto o "ya no evaluamos el lado del cliente", incluso si 2.2 evaluó perfectamente en el servidor. Por lo tanto, no esperaría que pasaran tiempo en cosas como esas, mientras que sus funciones principales están rotas. github.com/dotnet/efcore/issues/19830#issuecomment-584234667 para más detalles
TomTom
EfCore 3.1 roto, hay razones por las cuales el equipo de EfCore decidió no evaluar más el lado del cliente, incluso emiten advertencias al respecto en 2.2 para prepararlo para los próximos cambios. En cuanto a eso, no veo que esa cosa en particular esté rota. En cuanto a otras cosas que no puedo comentar, he visto problemas, pero he podido resolverlos sin ningún costo de rendimiento. Por otro lado, en los últimos 3 proyectos que hice para la producción, 2 de ellos estaban basados ​​en Dapper, uno basado en Ef ... Tal vez debería apuntar a la ruta apta para este, pero vence el propósito de entrada fácil para nuevos desarrolladores :-)... Ya veremos.
Yves Schelpe
El problema es la definición de qué es la evaluación del lado del servidor. Incluso soplan cosas muy simples que funcionaron sin problemas. Se arrancó la funcionalidad hasta que fue inútil. Simplemente eliminamos EfCore y volvemos a EF. EF + tercera parte para lfiltering global = trabajando. El problema con Dapper es que permito que cada usuario complejo decida LINQ: DEBO traducir eso desde el bo a una consulta del lado del servidor. Trabajó en Ef 2.2, totalmente borked ahora.
TomTom
Ok, ahora leo este github.com/dotnet/efcore/issues/19679#issuecomment-583650245 ... Ya veo lo que quieres decir ¿Qué libra de terceros estás usando entonces? ¿Podrías reformular lo que dijiste sobre Dapper, ya que no entendí lo que querías decir? Para mí funcionó, pero fueron proyectos de bajo perfil con solo 2 desarrolladores en el equipo, y una gran cantidad de manual para escribir para que funcione de manera eficiente, por supuesto ...
Yves Schelpe