В EF Core 8 узаконили Value Object 🤩



Одно из тех обновлений, которое не следует пропускать 😱



Например, есть следующая сущность заказа Order вместе с сущностью клиента Customer:



public class Customer

{

public int Id { get; set; }

public required string Name { get; set; }

public required Address Address { get; set; }

public List<Order> Orders { get; } = new();

}



public class Order

{

public int Id { get; set; }

public required string Contents { get; set; }

public required Address ShippingAddress { get; set; }

public required Address BillingAddress { get; set; }

public Customer Customer { get; set; } = null!;

}





Кажется, что уже существует семантика работы с типами значениями, ведь никто не отменял атрибут [Owned].



Однако, при попытке сохранить сущность, которая использует один инстанс на несколько полей возникнет ошибка:



var address = GetAddress();



customer.Orders.Add(

new Order { Contents = "Tesco Tasty Treats", BillingAddress = address, ShippingAddress = address, });



await context.SaveChangesAsync();





Кажется, что всё будет хорошо, но получим исключение InvalidOperationException с сообщением



«Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()»



Теперь, достаточно разметить тип данных атрибутом [ComplexType].

При этом он может быть иммутабельным!



[ComplexType]

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);





Соответственно, такое обновление сущности:



customer.Address = customer.Address with { Line1 = "Peacock Lodge" };



await context.SaveChangesAsync();





Сгенерирует следующий SQL:



UPDATE [Customers] SET [Address_Line1] = @p0

OUTPUT 1

WHERE [Id] = @p1;





То есть, это собственный тип на стероидах 💉