'Problem Changing the value of nested ValueObject using C# 10, .NET 6 and EF Core 6

My Aggregate is like this:

public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = new();

    public DateTime Date { get; set; }
    public IReadOnlyCollection<OrderItem> Items => _items;

    public void SetItemDiscounts(int itemId, IEnumerable<Discount> discounts)
    {
        var orderItem = _items.Single(item => item.Id == itemId);
        orderItem.SetDiscounts(discounts);
    }
}

public class OrderItem : Entity
{
    private readonly List<Discount> _discounts = new();

    public int OrderId { get; private set; }
    public int Qty { get; private set; }
    public Money Price { get; private set; }
    public IReadOnlyCollection<Discount> Discounts => _discounts;

    public void SetDiscounts(IEnumerable<Discount> discounts)
    {
        _discounts.Clear();
        _discounts.AddRange(discounts);
    }
}

public class Discount : ValueObject<Discount>
{
    public int Plan { get; private set; }
    public Money Amount { get; private set; }
}

public class Money : ValueObject<Money>
{
    public int CurrencyId { get; private set; }
    public decimal Value { get; private set; }
}

And the configuration for EF Core is:

protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().HasKey(x => x.Id);
        modelBuilder.Entity<Order>().Property(t => t.Id)
            .HasColumnType(nameof(SqlDbType.BigInt)).IsRequired();
        modelBuilder.Entity<Order>().Property(t => t.Date)
            .HasColumnType(nameof(SqlDbType.DateTime2)).IsRequired();

        modelBuilder.Entity<Order>().HasMany<OrderItem>().WithOne()
            .HasForeignKey(x => x.OrderId);

        modelBuilder.Entity<OrderItem>().HasKey(x => x.Id);
        modelBuilder.Entity<OrderItem>().Property(t => t.Id)
            .HasColumnType(nameof(SqlDbType.Int)).IsRequired();
        modelBuilder.Entity<OrderItem>().Property(t => t.Qty)
            .HasColumnType(nameof(SqlDbType.Int)).IsRequired();

        modelBuilder.Entity<OrderItem>().OwnsOne(x => x.Price)
            .Property(x => x.Value).HasColumnType(nameof(SqlDbType.Decimal))
            .HasColumnName("Price").IsRequired();

        modelBuilder.Entity<OrderItem>().OwnsMany(
            orderItem => orderItem.Discounts,
            discountNavigationBuilder =>
            {
                discountNavigationBuilder.Property(p => p.Plan)
                    .HasColumnType(nameof(SqlDbType.Int)).IsRequired();
                discountNavigationBuilder.OwnsOne(
                    discount => discount.Amount,
                    amountNavigationBuilder =>
                    {
                        amountNavigationBuilder.Property(p => p.CurrencyId)
                            .HasColumnType(nameof(SqlDbType.Int)).HasColumnName("CurrencyId").IsRequired();
                        amountNavigationBuilder.Property(p => p.Value)
                            .HasColumnType(nameof(SqlDbType.Decimal)).HasColumnName("Amount").IsRequired();
                    });
            });
    }

There is no problem when I create an order with Items and discounts, but while I update discounts using SetDiscounts method I get this error :

System.InvalidOperationException: The property 'Discount.Amount#Money.DiscountId' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.

What is DiscountId here? Is there any problem with my configuration or any other code?



Solution 1:[1]

After some search and study in EF-Core I found that there is a feature for ValueObjects. I changed the code and it works really fine:

public class Money : ValueObject<Money>
{
    public enum CurrencyType
    {
        USD = 1,
        IRI = 2
    }

    private Money(decimal value) => Value = value;

    public Money(decimal value, CurrencyType currencyType)
    {
        Value = Math.Round(value, currencyType == CurrencyType.USD ? 2 : 0);
    }

    public decimal Value { get; }

    public static Money Zero => new(0);
    
    public static implicit operator decimal(Money money) => money.Value;
    
    public static Money operator +(decimal a, Money b) => new (a + b.Value);

    public static Money operator +(Money a, decimal b) => new (a.Value + b);
}

I wrote a class fore value conversion:

public class MoneyValueConverter : ValueConverter<Money, decimal>
{
    public MoneyValueConverter() : base(
            money => money.Value,
            value => Money.Zero + value
        )
    {
    }
}

And I changed Ef configuration to :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>().HasKey(x => x.Id);
    modelBuilder.Entity<Order>().Property(t => t.Id)
        .HasColumnType(nameof(SqlDbType.BigInt)).IsRequired();
    modelBuilder.Entity<Order>().Property(t => t.Date)
        .HasColumnType(nameof(SqlDbType.DateTime2)).IsRequired();

    modelBuilder.Entity<Order>().HasMany<OrderItem>().WithOne()
        .HasForeignKey(x => x.OrderId);

    modelBuilder.Entity<OrderItem>().HasKey(x => x.Id);
    modelBuilder.Entity<OrderItem>().Property(t => t.Id)
        .HasColumnType(nameof(SqlDbType.Int)).IsRequired();
    modelBuilder.Entity<OrderItem>().Property(t => t.Qty)
        .HasColumnType(nameof(SqlDbType.Int)).IsRequired();

    modelBuilder.Entity<OrderItem>()
        .Property(t => t.Price)
        .HasConversion<MoneyValueConverter>()
        .HasColumnName(nameof(OrderItem.Price))
        .HasColumnType(nameof(SqlDbType.Decimal))
        .IsRequired();

    modelBuilder.Entity<OrderItem>().OwnsMany(
        orderItem => orderItem.Discounts,
        discountNavigationBuilder =>
        {
            discountNavigationBuilder
                .Property(p => p.Plan)
                .HasColumnType(nameof(SqlDbType.Int))
                .IsRequired();
            discountNavigationBuilder
                .Property(t => t.Amount)
                .HasConversion<MoneyValueConverter>()
                .HasColumnName(nameof(Discount.Amount))
                .HasColumnType(nameof(SqlDbType.Decimal))
                .IsRequired();
        });
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sadjad Abbasnia