ID fortement typés dans Entity Framework Core

12

J'essaie d'avoir une Idclasse fortement typée , qui tient désormais "longtemps" en interne. Mise en œuvre ci-dessous. Le problème que j'utilise dans mes entités est que Entity Framework me donne un message indiquant que l' ID de propriété y est déjà mappé. Voir monIEntityTypeConfiguration ci dessous.

Remarque: je ne vise pas à avoir une implémentation DDD rigide. Veuillez donc garder cela à l'esprit lorsque vous commentez ou répondez . L'identifiant derrière le dactylographié Idest destiné aux développeurs qui viennent au projet, ils sont fortement typés pour utiliser l'ID dans toutes leurs entités, bien sûr traduit en long(ouBIGINT ) - mais c'est clair pour les autres.

Ci-dessous la classe et la configuration, ce qui ne fonctionne pas. Le référentiel peut être trouvé sur https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31 ,

Idimplémentation de classe (marquée obsolète maintenant, car j'ai abandonné l'idée jusqu'à ce que je trouve une solution pour cela)

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

EntityTypeConfigurationJ'utilisais lorsque l'ID n'était pas marqué comme obsolète pour l'entitéPerson Malheureusement, quand de type ID, EfCore ne voulait pas le mapper ... quand de type long ce n'était pas un problème ... Autres types possédés, comme vous le voyez (avec Name) fonctionne 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 classe de base (quand j'utilisais toujours Id, donc quand il n'était pas marqué comme obsolète)

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(le domaine et les références aux autres ValueObjects peuvent être trouvés sur 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
la source

Réponses:

3

Je ne vise pas à avoir une implémentation DDD rigide. Veuillez donc garder cela à l'esprit lorsque vous commentez ou répondez. L'identifiant derrière l'identifiant typé est destiné aux développeurs qui viennent au projet, ils sont fortement typés pour utiliser l'identifiant dans toutes leurs entités

Alors pourquoi ne pas simplement ajouter un alias de type:

using Id = System.Int64;
David Browne - Microsoft
la source
Bien sûr, j'aime l'idée. Mais chaque fois que vous utiliserez l '"Id" dans un fichier .cs, ne devriez-vous pas vous assurer de placer cette instruction using là-haut - alors qu'une classe est transmise, il n'est pas nécessaire? De plus, je perdrais d'autres fonctionnalités de classe de base telles que Id.Empty..., ou je devrais l'implémenter autrement dans une méthode d'extension alors ... J'aime l'idée, merci de réfléchir. Si aucune autre solution ne se présente, je me contenterais de cela, car cela indique clairement l'intention.
Yves Schelpe
3

Donc, après avoir cherché longtemps et essayé d'obtenir une réponse supplémentaire, je l'ai trouvée, la voici. Merci à Andrew Lock.

ID fortement typés dans EF Core: utilisation d'ID d'entité fortement typés pour éviter l'obsession primitive - Partie 4 : https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity- ids à éviter-obsession-primitive-partie-4 /

TL; DR / Résumé d'Andrew Dans cet article, je décris une solution pour utiliser des ID fortement typés dans vos entités EF Core en utilisant des convertisseurs de valeur et un IValueConverterSelector personnalisé. Le ValueConverterSelector de base dans le cadre EF Core est utilisé pour enregistrer toutes les conversions de valeurs intégrées entre les types primitifs. En dérivant de cette classe, nous pouvons ajouter nos convertisseurs d'ID fortement typés à cette liste et obtenir une conversion transparente dans toutes nos requêtes EF Core

Yves Schelpe
la source
2

Je pense que vous n'avez pas de chance. Votre cas d'utilisation est extrêmement rare. Et EF Core 3.1.1 a encore du mal à mettre SQL dans la base de données qui n'est cassée dans rien, sauf dans la plupart des cas de base.

Donc, vous devriez écrire quelque chose qui passe par l'arborescence LINQ et c'est probablement une énorme quantité de travail, et si vous tombez sur des bugs sur EF Core - que vous aurez - amusez-vous à expliquer cela dans vos tickets.

TomTom
la source
Je suis d'accord que le cas d'utilisation est rare, mais l'idée derrière elle n'est pas entièrement stupide, j'espère peut-être ...? Si oui, faites-le moi savoir. Si c'est stupide (convaincu jusqu'à présent non, car les identifiants fortement typés sont si faciles à programmer dans le domaine), ou si je ne trouve pas de réponse rapidement, je pourrais utiliser un alias comme suggéré par David Browne - Micrososft ci-dessous ( stackoverflow .com / a / 60155275/1155847 ). Jusqu'ici tout va bien sur les autres cas d'utilisation, et les collections et les champs cachés dans EF Core, pas de bugs, donc j'ai trouvé ça étrange, car sinon j'ai une bonne expérience avec le produit.
Yves Schelpe
Ce n'est pas stupide en soi, mais il est rare que AUCUN orme que j'aie jamais vu ne le supporte et EfCore est si mauvais qu'en ce moment, je travaille à le supprimer et à revenir à Ef (non core) parce que j'ai besoin d'expédier. Pour moi, EfCore 2.2 a mieux fonctionné - 3.1 est 100% inductible car toute projection que j'utilise entraîne un mauvais sql ou "nous n'évaluons plus le côté client" même si - 2.2 a parfaitement évalué sur le serveur. Donc, je ne m'attendrais pas à ce qu'ils passent du temps sur des choses comme ça - alors que leurs fonctions de base sont brisées. github.com/dotnet/efcore/issues/19830#issuecomment-584234667 pour plus de détails
TomTom
EfCore 3.1 cassé, il y a des raisons pour lesquelles l'équipe EfCore a décidé de ne plus évaluer le côté client, elle émet même des avertissements à ce sujet en 2.2 pour vous préparer aux changements à venir. Quant à cela, je ne vois pas que cette chose en particulier est cassée. Quant à d'autres choses que je ne peux pas commenter, j'ai vu des problèmes, mais j'ai pu les résoudre sans aucun coût de perf. D'un autre côté, sur les 3 derniers projets que j'ai réalisés pour la production, 2 d'entre eux étaient basés sur Dapper, un basé sur Ef ... :-)... Nous verrons.
Yves Schelpe
Le problème est la définition de ce qu'est l'évaluation côté serveur. Ils soufflent même sur des trucs très simples qui fonctionnaient parfaitement. Fonctionnalité arrachée jusqu'à ce qu'elle soit utilisée. Nous supprimons simplement EfCore et revenons à EF. EF + tierce partie pour filtrage global = travail. Le problème avec Dapper est que j'autorise chaque utilisateur complexe à décider LINQ - je DOIS traduire cela du bo en une requête côté serveur. A travaillé dans Ef 2.2, totalement borked maintenant.
TomTom
Ok, je lis maintenant ce github.com/dotnet/efcore/issues/19679#issuecomment-583650245 ... Je vois ce que vous voulez dire Quelle bibliothèque tierce utilisez-vous alors? Pourriez-vous reformuler ce que vous avez dit à propos de Dapper, car je ne comprenais pas ce que vous vouliez dire. Pour moi, cela a fonctionné, mais c'étaient des projets discrets avec seulement 2 développeurs dans l'équipe - et beaucoup de passe-partout manuels à écrire pour le faire fonctionner bien sûr ...
Yves Schelpe