IDs fortemente digitados no Entity Framework Core

12

Estou tentando ter uma Idclasse fortemente tipada , que agora dura 'internamente'. Implementação abaixo. O problema que estou usando isso em minhas entidades é que o Entity Framework me dá uma mensagem de que o ID da propriedade já está mapeado para ele. Veja meu IEntityTypeConfigurationabaixo.

Nota: Não pretendo ter uma implementação rígida de DDD. Portanto, lembre-se disso ao comentar ou responder . Todo o ID por trás do digitado Idé para os desenvolvedores que vêm para o projeto, eles são fortemente tipados para usar o Id em todas as suas entidades, é claro traduzido para long(ou BIGINT) - mas fica claro para outros.

Abaixo da classe e configuração, que não funciona. O repositório pode ser encontrado em https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31 ,

Idimplementação de classe (marcada como obsoleta agora, porque abandonei a ideia até encontrar uma solução para isso)

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

EntityTypeConfigurationEu estava usando quando o ID não foi marcado como obsoleto para entidadePerson Infelizmente, porém, quando do tipo Id, o EfCore não quis mapeá-lo ... quando do tipo longo, não houve problema ... Outros tipos de propriedade, como você vê (com Name) funciona bem.

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 classe base (quando eu ainda estava usando o Id, então quando não estava 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(o domínio e as referências aos outros ValueObjects podem ser encontrados em 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
fonte

Respostas:

3

Não pretendo ter uma implementação rígida de DDD. Portanto, lembre-se disso ao comentar ou responder. Todo o ID por trás do ID digitado é para os desenvolvedores que chegam ao projeto, eles são fortemente tipados para usar o ID em todas as suas entidades

Por que não adicionar um alias de tipo:

using Id = System.Int64;
David Browne - Microsoft
fonte
Claro, eu gosto da ideia. Mas toda vez que você usará o "Id" em um arquivo .cs, não precisará colocar essa declaração de uso no topo - enquanto uma classe é passada por aí, não é necessário? Além disso, eu perderia outras funcionalidades da classe base, como Id.Empty..., ou teria que implementá-lo de outra forma em um método de extensão ... Gosto da idéia, thx por pensar nisso. Se nenhuma outra solução surgir, eu me contentaria com isso, pois isso indica claramente a intenção.
Yves Schelpe
3

Então, depois de pesquisar um longo tempo e tentar obter mais respostas, encontrei, aqui está então. Obrigado a Andrew Lock.

IDs fortemente tipados no EF Core: usando IDs de entidades fortemente tipados para evitar obsessão primitiva - Parte 4 : https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity- ids-para-evitar-obsessão-primitiva-parte-4 /

TL; DR / Resumo de Andrew Neste post, descrevo uma solução para o uso de IDs fortemente tipados em suas entidades EF Core usando conversores de valor e um IValueConverterSelector personalizado. O ValueConverterSelector base na estrutura EF Core é usado para registrar todas as conversões de valor internas entre tipos primitivos. Ao derivar dessa classe, podemos adicionar nossos conversores de ID fortemente tipados a esta lista e obter uma conversão contínua em todas as nossas consultas EF Core

Yves Schelpe
fonte
2

Eu acho que você está sem sorte. Seu caso de uso é extremamente raro. E o EF Core 3.1.1 ainda está lutando para colocar o SQL no banco de dados que não está quebrado em nada, exceto nos casos mais básicos.

Então, você teria que escrever algo que atravesse a árvore LINQ e isso provavelmente é uma tremenda quantidade de trabalho, e se você encontrar erros no EF Core - o que você vai - se divertir explicando isso nos seus tickets.

TomTom
fonte
Concordo que o caso de uso é raro, mas a idéia por trás dele não é totalmente estúpida, espero ...? Se sim, por favor me avise. Se for estúpido (convencido até agora, como IDs fortemente tipados são tão fáceis de programar no domínio), ou se eu não encontrar uma resposta rapidamente, posso usar um alias, conforme sugerido por David Browne - Micrososft abaixo ( stackoverflow .com / a / 60155275/1155847 ). Até agora tudo bem em outros casos de uso e coleções e campos ocultos no EF Core, sem bugs, então achei estranho, pois caso contrário, tenho uma sólida experiência com o produto.
Yves Schelpe
Não é estúpido por si só, mas é raro o suficiente que nenhuma empresa que eu já tenha visto o apóie e o EfCore seja tão ruim que, no momento, estou trabalhando para removê-lo e voltar para o Ef (não essencial) porque preciso enviar. Para mim, o EfCore 2.2 funcionou melhor - o 3.1 é 100% inutável, já que qualquer projeção que eu use resulta em sql ruim ou "não avaliamos mais o lado do cliente", mesmo que o 2.2 tenha sido perfeitamente avaliado no servidor. Portanto, eu não esperaria que eles gastassem tempo com coisas assim - enquanto suas principais funções estiverem quebradas. github.com/dotnet/efcore/issues/19830#issuecomment-584234667 para obter mais detalhes
TomTom
O EfCore 3.1 está quebrado, há razões pelas quais a equipe do EfCore decidiu não avaliar mais o lado do cliente; eles até emitem avisos sobre isso no 2.2 para prepará-lo para as próximas mudanças. Quanto a isso, não vejo que algo em particular esteja quebrado. Quanto a outras coisas que não posso comentar, já vi problemas, mas consegui resolvê-los sem nenhum custo de desempenho. Por outro lado, nos últimos 3 projetos que fiz para a produção, 2 deles foram baseados no Dapper, um no Ef ... Talvez eu deva procurar seguir o caminho mais elegante para este, mas derrota o objetivo da entrada fácil para novos desenvolvedores :-)... Veremos.
Yves Schelpe
O problema é a definição do que é a avaliação do servidor. Eles até fazem coisas muito simples que funcionam perfeitamente. Rasgou a funcionalidade até que fosse useles. Acabamos de remover o EfCore e voltamos para a EF. EF + terceiros para filtragem global = funcionando. O problema com o dapper é que eu permito que todos os usuários complexos decidam o LINQ - DEVO traduzir isso do bo em uma consulta no servidor. Trabalhou na Ef 2.2, totalmente trabalhado agora.
TomTom
Ok, agora eu li este github.com/dotnet/efcore/issues/19679#issuecomment-583650245 ... Entendo o que você quer dizer Com qual lib de terceiros você está usando? Você poderia reformular o que disse sobre Dapper, porque eu não entendi o que você quis dizer. Para mim, funcionou, mas foram os projetos menos importantes, com apenas 2 desenvolvedores da equipe - e muitos clichês manuais para escrever para que funcionem com eficiência, é claro ...
Yves Schelpe