Silnie wpisane identyfikatory w rdzeniu Entity Framework

12

Staram się mieć silnie napisaną Idklasę, która teraz wewnętrznie utrzymuje „długi”. Realizacja poniżej. Problem polegający na używaniu tego w moich jednostkach polega na tym, że Entity Framework daje mi komunikat, że identyfikator właściwości jest już na nim zmapowany. Zobacz moje IEntityTypeConfigurationponiżej.

Uwaga: Nie zamierzam mieć sztywnej implementacji DDD. Więc proszę o tym pamiętać, gdy komentując odebraniem . Cały identyfikator wpisany Idjest dla programistów przybywających do projektu, którzy są mocno wpisani, aby używać Id we wszystkich swoich obiektach, oczywiście przetłumaczone na long(lub BIGINT) - ale jest to jasne dla innych.

Poniżej klasy i konfiguracji, która nie działa. Repozytorium można znaleźć na stronie https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31 ,

Idimplementacja klasy (teraz oznaczona jako przestarzała, ponieważ porzuciłem pomysł, dopóki nie znalazłem rozwiązania)

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

EntityTypeConfigurationPersonUżywałem, gdy nie oznaczono identyfikatora jako przestarzały dla bytu Niestety jednak, gdy typu Id, EfCore nie chciał go zmapować ... kiedy typu był długi, nie było problemu ... Inne posiadane typy, jak widzisz (z Name) dobrze pracować.

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 klasa podstawowa (kiedy wciąż korzystałem z Id, więc kiedy nie było zaznaczone jako przestarzałe)

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(domenę i odniesienia do innych ValueObjects można znaleźć na stronie 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
źródło

Odpowiedzi:

3

Nie zamierzam mieć sztywnej implementacji DDD. Pamiętaj o tym, komentując lub odpowiadając. Cały identyfikator za wpisanym identyfikatorem jest przeznaczony dla programistów przybywających do projektu, którzy są mocno wpisani, aby używać identyfikatora we wszystkich swoich jednostkach

Dlaczego więc nie po prostu dodać alias typu:

using Id = System.Int64;
David Browne - Microsoft
źródło
Jasne, podoba mi się ten pomysł. Ale za każdym razem, gdy użyjesz „Id” w pliku .cs, nie musisz upewnić się, że umieściłeś tam tę instrukcję za pomocą na górze - chociaż podczas przekazywania klasy nie trzeba? Również straciłbym inną funkcjonalność klasy bazowej, taką jak Id.Empty..., lub musiałbym ją zaimplementować w metodzie rozszerzenia, a następnie ... Podoba mi się pomysł, dzięki za myślenie dalej. Jeśli nie znajdzie się żadne inne rozwiązanie, zadowoliłbym się tym, ponieważ wyraźnie stwierdza to zamiar.
Yves Schelpe,
3

Po długich poszukiwaniach i próbie uzyskania dodatkowej odpowiedzi znalazłem ją, oto ona. Dzięki Andrew Lock.

Silnie typowane identyfikatory w EF Core: Używanie silnie typowanych identyfikatorów bytów, aby uniknąć prymitywnej obsesji - Część 4 : https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity- ids-to-unikać-prymitywne-obsesja-część-4 /

TL; DR / Podsumowanie Andrew W tym poście opisuję rozwiązanie polegające na użyciu silnie typowanych identyfikatorów w jednostkach EF Core za pomocą konwerterów wartości i niestandardowego IValueConverterSelector. Podstawowy ValueConverterSelector we frameworku EF Core służy do rejestrowania wszystkich wbudowanych konwersji wartości między typami pierwotnymi. Korzystając z tej klasy, możemy dodać do tej listy nasze silnie typowane konwertery ID i uzyskać płynną konwersję w naszych zapytaniach EF Core

Yves Schelpe
źródło
2

Myślę, że nie masz szczęścia. Twój przypadek użycia jest niezwykle rzadki. EF Core 3.1.1 wciąż ma problemy z umieszczeniem SQL w bazie danych, która nie jest zepsuta w niczym innym niż w najbardziej podstawowych przypadkach.

Musiałbyś więc napisać coś, co przejdzie przez drzewo LINQ, a to prawdopodobnie ogromna ilość pracy, a jeśli natkniesz się na błędy w EF Core - co będziesz - dobrze się bawić, tłumacząc to na swoich biletach.

TomTom
źródło
Zgadzam się, że przypadek użycia jest rzadki, ale pomysł, który za tym stoi, nie jest do końca głupi, mam nadzieję ... Jeśli tak, proszę dać mi znać. Jeśli jest głupi (jak dotąd nie przekonany, ponieważ silnie typowane identyfikatory są tak łatwe do zaprogramowania w domenie), lub jeśli nie znajdę odpowiedzi szybko, mógłbym użyć aliasu sugerowanego przez Davida Browne'a - Micrososft poniżej ( stackoverflow .pl / a / 60155275/1155847 ). Do tej pory dobre w innych przypadkach użycia, kolekcjach i ukrytym polu w EF Core, bez błędów, więc pomyślałem, że to dziwne, ponieważ w przeciwnym razie mam solidne dobre doświadczenie z produktem.
Yves Schelpe,
Nie jest to głupie samo w sobie, ale rzadko zdarza się, że ŻADNE orm, które kiedykolwiek widziałem, wspiera to, a EfCore jest tak zły, że teraz pracuję nad usunięciem go i powrotem do Ef (non core), ponieważ muszę wysłać. Dla mnie EfCore 2.2 działał lepiej - 3.1 jest w 100% niestabilny, ponieważ jakakolwiek projekcja, której używam, powoduje zły kod SQL lub „już nie oceniamy po stronie klienta”, nawet jeśli - 2.2 doskonale ocenił na serwerze. Nie spodziewałbym się więc, że będą spędzać czas na takich rzeczach - podczas gdy ich podstawowe funkcje są zepsute. github.com/dotnet/efcore/issues/19830#issuecomment-584234667 po więcej szczegółów
TomTom
Zepsuty EfCore 3.1, istnieją powody, dla których zespół EfCore postanowił nie oceniać po stronie klienta, nawet wydaje ostrzeżenia o nim w 2.2, aby przygotować cię na nadchodzące zmiany. Jeśli chodzi o to, nie widzę, aby ta konkretna rzecz była zepsuta. Co do innych rzeczy, których nie mogę komentować, widziałem problemy, ale udało mi się je rozwiązać bez żadnych kosztów. Z drugiej strony, w ostatnich 3 projektach, które zrobiłem dla produkcji, 2 z nich były oparte na Dapper, jeden oparty na Ef ... Może powinienem dążyć do tego, aby przejść ten elegancki, ale pokonuje cel łatwego wejścia dla nowych deweloperów :-)... Zobaczymy.
Yves Schelpe,
Problemem jest definicja oceny po stronie serwera. Wydmuchują nawet bardzo proste rzeczy, które działały bezbłędnie. Wyrwał funkcjonalność, dopóki nie był użyteczny. Po prostu usuwamy EfCore i wracamy do EF. EF + firma zewnętrzna dla globalnego filtrowania plików = działająca. Problem z wytwornością polega na tym, że pozwalam każdemu złożonemu użytkownikowi zdecydować o LINQ - MUSZĘ przetłumaczyć to z bo na zapytanie po stronie serwera. Pracowałem w Ef 2.2, teraz całkowicie rozwalony.
TomTom
Ok, teraz czytam to github.com/dotnet/efcore/issues/19679#issuecomment-583650245 ... Rozumiem, co masz na myśli Z jakiej biblioteki osób trzecich korzystasz wtedy? Czy mógłbyś przeformułować to, co powiedziałeś o Dapperie, ponieważ nie rozumiałem, co masz na myśli? Dla mnie to zadziałało, ale były to projekty o niskim poziomie kluczowym, z zaledwie 2 deweloperami w zespole - i dużą ilością ręcznych instrukcji do pisania, aby działało to oczywiście ...
Yves Schelpe