Ответ
Сравнение сущностей в EF Core имеет нюансы из-за механизма отслеживания изменений (change tracking). Вот основные подходы:
1. Сравнение по первичному ключу (Id) — основной способ для сущностей Сущность определяется своим идентификатором, а не значениями полей. Это согласуется с концепцией Entity в DDD.
public class Product : IEquatable<Product>
{
public int Id { get; set; }
public string Name { get; set; }
public bool Equals(Product other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Сравниваем только Id, даже если сущности не отслеживаются
return Id != 0 && Id == other.Id;
}
public override bool Equals(object obj) => Equals(obj as Product);
public override int GetHashCode() => Id.GetHashCode();
}
// Использование
var product1 = await context.Products.FindAsync(1);
var product2 = new Product { Id = 1 };
bool areEqual = product1.Equals(product2); // true, т.к. Id совпадают
2. Учет состояния отслеживания (DbContext) EF Core считает две ссылки равными, если они представляют одну и ту же отслеживаемую сущность.
var p1 = await context.Products.FirstAsync(p => p.Id == 1);
var p2 = await context.Products.FirstAsync(p => p.Id == 1);
// p1 == p2 будет true, если запрос использовал отслеживание (AsNoTracking не применялся)
// и оба объекта получены в одном экземпляре DbContext.
3. Сравнение объектов-значений (Value Objects) Для вложенных объектов, которые определяются всеми своими полями, нужно сравнивать каждое свойство.
public record Address(string Street, string City, string ZipCode); // Используем record для автоматического сравнения
// Или вручную:
public class Address : IEquatable<Address>
{
public string Street { get; init; }
...
public bool Equals(Address other) => other != null &&
Street == other.Street &&
City == other.City &&
ZipCode == other.ZipCode;
public override int GetHashCode() => HashCode.Combine(Street, City, ZipCode);
}
Ключевое правило: Для сущностей сравнивайте по Id. Для объектов-значений сравнивайте по всем значимым полям. Всегда переопределяйте GetHashCode вместе с Equals.
Ответ 18+ 🔞
Ну ты смотри, какие тонкости всплывают, когда начинаешь сравнивать сущности в EF Core! Это ж не просто так, взять и через == их ткнуть. Там под капотом целый цирк с отслеживанием изменений разворачивается, блядь.
Смотри, главная идея, которую надо в башку вбить: сущность (Entity) и объект-значение (Value Object) — это две большие разницы, и сравниваются они по-разному, как водка и пиво. И если это перепутать, будет тебе веселье, в рот меня чих-пых.
Первый подход, для сущностей: сравниваем по ID, и всё тут.
Сущность — это как паспорт. Её определяет номер, а не то, как она выглядит или как её зовут. Поменял имя — всё равно тот же человек. Поэтому сравниваем только по Id. Вот смотри, как это выглядит в коде:
public class Product : IEquatable<Product>
{
public int Id { get; set; }
public string Name { get; set; }
public bool Equals(Product other)
{
// Если другой объект — пустое место, сразу false
if (ReferenceEquals(null, other)) return false;
// Если это один и тот же объект в памяти — очевидно, true
if (ReferenceEquals(this, other)) return true;
// А вот тут магия: сравниваем ТОЛЬКО ID.
// Id != 0 проверяет, что это вообще сохранённая сущность, а не новая.
return Id != 0 && Id == other.Id;
}
public override bool Equals(object obj) => Equals(obj as Product);
// GetHashCode ОБЯЗАТЕЛЬНО переопределяем, иначе всё сломается в коллекциях!
public override int GetHashCode() => Id.GetHashCode();
}
// Используем
var product1 = await context.Products.FindAsync(1); // Вытащили из базы
var product2 = new Product { Id = 1 }; // Создали вручную с таким же ID
bool areEqual = product1.Equals(product2); // true, ёпта! Потому что ID совпали.
Второй момент, про отслеживание (Change Tracking). Это вообще отдельная песня. EF Core, тот ещё затейник, он может считать две ссылки равными, если они указывают на одну и ту же отслеживаемую сущность в его памяти. Смотри:
var p1 = await context.Products.FirstAsync(p => p.Id == 1);
var p2 = await context.Products.FirstAsync(p => p.Id == 1);
// Интересный вопрос: p1 == p2?
// Будет true, если оба запроса были без AsNoTracking() и выполнены в рамках ОДНОГО И ТОГО ЖЕ экземпляра DbContext.
// Потому что контекст умный, он вернёт тебе ссылку на один и тот же объект, который уже отслеживает.
// А если контексты разные или стоит AsNoTracking — то это будут два разных объекта в памяти, и == даст false, хотя в базе-то одна запись!
Третий случай, для объектов-значений (Value Objects). Вот тут уже не отделаешься одним ID. Объект-значение определяется ВСЕМИ своими полями целиком. Адрес, деньги, цвет — если хоть что-то поменялось, это уже другое значение. Тут надо сравнивать всё подряд.
Самый лёгкий способ в современных шарагах — использовать record. Это такая волшебная структура, которая сама за тебя всё сравнение и хеш-код сделает.
public record Address(string Street, string City, string ZipCode);
// Всё! Equals, GetHashCode уже реализованы, сравниваются все поля.
// Красота, блядь.
Ну а если ты старовер и не доверяешь рекордам, делай вручную:
public class Address : IEquatable<Address>
{
public string Street { get; init; }
public string City { get; init; }
public string ZipCode { get; init; }
public bool Equals(Address other)
{
if (other is null) return false;
// Сравниваем ВСЕ значимые поля, до последней запятой!
return Street == other.Street &&
City == other.City &&
ZipCode == other.ZipCode;
}
// GetHashCode тоже ОБЯЗАТЕЛЬНО! Иначе в словарях и хеш-сетах будет полный пиздец.
public override int GetHashCode() => HashCode.Combine(Street, City, ZipCode);
}
Итоговая мудрость, которую надо выжечь на подкорке:
- Для сущностей (Entity) — сравнивай только по
Id. Они как люди — важна суть, а не внешность. - Для объектов-значений (Value Object) — сравнивай по всем полям. Они как паззл — если деталь другая, то и картинка другая.
- И никогда, слышишь, НИКОГДА не забывай переопределять
GetHashCode(), когда переопределяешьEquals. Иначе твои объекты в коллекциях будут вести себя как угорелые, и ты потом полдня будешь искать, почемуDictionaryне находит ключ, который вроде бы там есть.