Сильно типизированные идентификаторы в ядре Entity Framework

12

Я пытаюсь иметь строго типизированный Idкласс, который теперь содержит «long» внутри. Реализация ниже. Проблема, которую я использую в своих объектах, заключается в том, что Entity Framework дает мне сообщение о том, что идентификатор свойства уже сопоставлен с ним. Смотрите мой IEntityTypeConfigurationниже.

Примечание: я не собираюсь иметь жесткую реализацию DDD. Поэтому, пожалуйста, имейте это в виду при комментировании или ответе . Весь идентификатор, напечатанный за напечатанным, Idпредназначен для разработчиков, приходящих в проект, для которых они строго типизированы, чтобы использовать Id во всех своих сущностях, конечно, переведенные в long(или BIGINT), но тогда это очевидно для других.

Ниже класс и конфигурация, которая не работает. Репо можно найти по адресу https://github.com/KodeFoxx/Kf.CleanArchitectureTemplate.NetCore31 ,

Idреализация класса (помечена как устаревшая, потому что я отказался от идеи, пока не нашел решение для этого)

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Я использовал, когда Id не помечен как устаревший для сущности.Person К сожалению, когда для типа Id EfCore не хотел отображать его ... когда для типа long это не было проблемой ... Другие принадлежащие типы, как вы видите (с Name) отлично работает

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 базовый класс (когда я все еще использовал Id, поэтому, когда он не был помечен как устаревший)

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(домен и ссылки на другие объекты ValueObject можно найти по адресу 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);
    }
}
Ив Шелпе
источник

Ответы:

3

Я не собираюсь иметь жесткую реализацию DDD. Поэтому, пожалуйста, имейте это в виду при комментировании или ответе. Весь идентификатор за напечатанным идентификатором предназначен для разработчиков, приходящих в проект, для которых они строго типизированы для использования идентификатора во всех своих объектах.

Тогда почему бы просто не добавить псевдоним типа:

using Id = System.Int64;
Дэвид Браун - Microsoft
источник
Конечно, мне нравится идея. Но каждый раз, когда вы будете использовать «Id» в файле .cs, не нужно ли обязательно размещать его с помощью оператора using на вершине - в то время как с передаваемым классом не нужно? Также я потерял бы другие функциональные возможности базового класса, такие как Id.Empty..., или должен был бы реализовать это иначе в методе расширения тогда ... Мне нравится идея, спасибо за размышления. Если не будет найдено никакого другого решения, я бы согласился на это, так как это ясно говорит о намерении.
Ив Шелпе
3

Так что после долгих поисков и попыток получить какой-то ответ я нашел его, вот он. Спасибо Эндрю Локу.

Сильно типизированных идентификаторы в EF Ядро: Использование строго типизированных идентификаторов сущностей , чтобы избежать примитивной одержимость - Часть 4 : https://andrewlock.net/strongly-typed-ids-in-ef-core-using-strongly-typed-entity- идентификаторы к Avoid-примитивно-одержимость частей-4 /

TL; DR / Сводка Эндрю В этом посте я опишу решение использования строго типизированных идентификаторов в ваших сущностях EF Core с помощью преобразователей значений и пользовательского IValueConverterSelector. Базовый ValueConverterSelector в платформе EF Core используется для регистрации всех встроенных преобразований значений между примитивными типами. Исходя из этого класса, мы можем добавить наши строго типизированные преобразователи идентификаторов в этот список и получить плавное преобразование во всех наших запросах EF Core.

Ив Шелпе
источник
2

Я думаю, что вам не повезло. Ваш случай использования крайне редок. И EF Core 3.1.1 все еще борется с размещением SQL в базе данных, которая не сломана ни в чем, кроме самых базовых случаев.

Итак, вам придется написать что-то, что проходит через дерево LINQ, и это, вероятно, огромная работа, и если вы наткнетесь на ошибки в EF Core - что вы будете - весело проведете время, объясняя это в своих билетах.

TomTom
источник
Я согласен, что случай использования встречается редко, но идея, стоящая за ним, не совсем глупая, я могу надеяться ...? Если это так, пожалуйста, дайте мне знать. Если это глупо (до сих пор не убеждено, так как строго типизированные идентификаторы так легко программировать в домене), или если я не могу быстро найти ответ, я мог бы использовать псевдоним, предложенный Дэвидом Брауном - Micrososft ниже ( stackoverflow .com / a / 60155275/1155847 ). Пока что все хорошо в других случаях использования, а также в коллекциях и скрытых полях в EF Core, никаких ошибок, поэтому я подумал, что это странно, так как в остальном у меня хороший опыт работы с продуктом.
Ив Шелпе
Само по себе это не глупо, но редко бывает так, что ни одна форма, которую я когда-либо видел, не поддерживает его, а EfCore настолько плох, что сейчас я работаю над его удалением и возвращением в Ef (не ядро), потому что мне нужно отправить. Для меня EfCore 2.2 работал лучше - 3.1 на 100% не пригоден для использования, поскольку любая проекция, которую я использую, приводит к плохому sql или «мы больше не оцениваем клиентскую сторону», даже если - 2.2 отлично оценивал на сервере. Так что я бы не ожидал, что они будут тратить время на подобные вещи - пока их основные функции нарушены. github.com/dotnet/efcore/issues/19830#issuecomment-584234667 для более подробной информации
TomTom
EfCore 3.1 не работает, есть причины, по которым команда EfCore решила больше не оценивать клиентскую сторону, они даже выдают предупреждения об этом в 2.2, чтобы подготовить вас к предстоящим изменениям. Что касается этого, я не вижу, что эта конкретная вещь сломана. Что касается других вещей, которые я не могу комментировать, я видел проблемы, но смог решить их без каких-либо затрат. С другой стороны, в последних 3 проектах, которые я делал для производства, 2 из них были на основе Dapper, один на основе Ef ... Может быть, я должен стремиться пойти по этому пути более щадящим, но побеждать цель легкого входа для новых разработчиков :-)... Посмотрим.
Ив Шелпе
Проблема заключается в определении того, что такое оценка на стороне сервера. Они даже дуют на очень простые вещи, которые работают без нареканий. Вырванный функционал, пока не было гусениц. Мы просто удаляем EfCore и возвращаемся к EF. EF + третье лицо для глобального lfiltering = работает. Проблема с dapper заключается в том, что я разрешаю каждому сложному пользователю решить LINQ - я ДОЛЖЕН перевести это из bo в запрос на стороне сервера. Работал в Ef 2.2, теперь полностью потерян.
TomTom
Хорошо, теперь я читаю этот github.com/dotnet/efcore/issues/19679#issuecomment-583650245 ... Я понимаю, что вы имеете в виду. Какую стороннюю библиотеку вы используете тогда? Не могли бы вы перефразировать то, что вы сказали о Даппере, поскольку я не поняла, что вы имели в виду. Для меня это сработало, но это были проекты, которые были сдержанными, когда в команде было всего 2 разработчика - и много ручных шаблонов, чтобы писать, чтобы это работало эффективно, конечно ...
Ив Шелпе