Как правильно сравнивать сущности (Entity) в Entity Framework?

«Как правильно сравнивать сущности (Entity) в Entity Framework?» — вопрос из категории Entity Framework, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Сравнение сущностей в 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.