0

I have a value object and an entity that uses said value object. I'm using the null-object pattern for said value object. My value object looks something like the following:

public class Reference {
  public static Reference Empty;
  private string _value;

  static Reference() {
    Empty = new Reference(String.Empty);
  }
  public Reference(string value) {
    _value = value;
  }
  private Reference() {
    _value = default!;
  }
  public string Value => _value;
}

And my entity:

public class Payment {
  private Guid _id;
  private decimal _amount;
  private Reference _reference;

  public Payment(decimal amount) {
    _id = Guid.Empty; //ef will generate the id
    _reference = Reference.Empty;
    _amount = amount;
  }
  private Payment() {
    _id = default;
    _reference = default!;
    _amount = default;
  }

  public Guid Id => _id;
  public decimal Amount => _amount;
  public Reference Reference => _reference;
}

Now, the class Reference is configured in EF-Core using OwnsOne relationship. Something like this:

internal class PaymentConfiguration
  : IEntityTypeConfiguration<Payment> {

  public void Configure(EntityTypeBuilder<Payment> builder) {
    builder.ToTable("Payments");
    builder.HasKey(x => x.Id);
    builder.OwnsOne(x => x.Reference, y => {
      y.Property(z => z.Value)
        .HasColumnName("Reference")
        .IsRequired();
    });
  }
}

Adding a payment looks like the following:

var payment = new Payment(5000m);
_context.Payments.Add(payment);
await _context.SaveChangesAsync();

This works pretty well, but the moment I try to add two payments:

_context.Payments.Add(new Payment(5000m));
_context.Payments.Add(new Payment(6000m));
await _context.SaveChangesAsync();

I get an error with the following message: The property 'PaymentId' on entity type 'Reference' 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' then associate the dependent with the new principal.

After so much debugging, I found out that the problem is that both Payment objects uses the same reference object defined by Reference.Empty. Indeed, if I change my code to the following, the problem stops:

  public Payment(decimal amount) {
    _id = Guid.Empty; //ef will generate the id
    _reference = new Reference(String.Empty); //instead of Reference.Empty;
    _amount = amount;
  }

I don't know the inner workings of EF-Core, but if I had to guess, I'd say that somehow EF-Core creates a derived class and adds stuff to make it work, therefore my Reference.Empty object is changed thus affecting every Payment object using it. This means that I cannot use the null-object pattern as is.

Now, my question. Is there a way to configure Ef-Core to stop this behaviour? Or is there a workaround for using the null-object pattern? I'm using C#8, .NET Core 3 and Entity Framework Core 3. Thanks in advance.

Pedro Coelho
  • 1,411
  • 3
  • 18
  • 31
Fernando Gómez
  • 464
  • 1
  • 5
  • 19
  • Did you find a solution to this? – Tobias Jul 15 '21 at 14:26
  • @Tobias a workaround of sorts. I ended up changing the definition of `Reference.Empty`, instead of using a static field, I used a method and return a new object. Something like `public static Reference Empty() => new Reference(String.Empty);`. – Fernando Gómez Jul 15 '21 at 17:35
  • @Tobias It seems the problem is that EF tracks instances so having the two objects pointing at the same instance is a no-go. So I don't think this problem will ever be resolved, as it probably require EF to change its inner core. Thus I settled with this workaround. – Fernando Gómez Jul 15 '21 at 17:43

1 Answers1

-1

In EF Core, an alternate key is designed to be used as the target of a relationship (i.e. there will be a foreign key that points to it). Currently EF does not support changing the values - though they have this issue below tracking enabling this in the future.

https://github.com/dotnet/efcore/issues/4073

If you just want a unique index on the property, then use this code:

modelBuilder.Entity().HasIndex(u => u.Name).IsUnique();

Edit:

When configuring ownsone, you are creating an implicit key to Reference, based on Payment (PaymentId key, on this case). That's why the compiler is interpretating it as such and blowing the Exception "The property 'PaymentId' on entity type 'Reference' [...]"

Check out Microsoft's doc on this:

Owned types configured with OwnsOne or discovered through a reference navigation always have a one-to-one relationship with the owner, therefore they don't need their own key values as the foreign key values are unique.

https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities#implicit-keys

Finally, I suggest you inherit the ValueObject class instead of instantiating it. Also, make your value object class as an abstract class and eliminate OwnsOne, in the code below:

builder.OwnsOne(x => x.Reference, y => {
      y.Property(z => z.Value)
        .HasColumnName("Reference")
        .IsRequired();
    });
Pedro Coelho
  • 1,411
  • 3
  • 18
  • 31
  • But I'm not trying to change any ID. Actually, the class Reference doesn't even have an ID. – Fernando Gómez Feb 25 '20 at 22:34
  • are you trying to save an entity with no id? How will you identify it? – Pedro Coelho Feb 25 '20 at 22:36
  • The class Payment has an Id. The class Reference (which I'm using as a value object, following DDD principles) does not have an Id. Since the error message is mentioning Reference.PaymentId, I'm assuming EFCore is generating said property, but my code is not using any. – Fernando Gómez Feb 25 '20 at 22:36
  • When you work always on the same Reference reference you are always dealing with the same PaymentId, aparently – Pedro Coelho Feb 25 '20 at 22:41
  • I suggest you inherit the ValueObject class instead of instantiating it – Pedro Coelho Feb 25 '20 at 22:44
  • and also, make your value object class as an abstract class – Pedro Coelho Feb 25 '20 at 22:49
  • @FernandoGómez check this out, I found out the problem: https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities#implicit-keys – Pedro Coelho Feb 25 '20 at 22:53
  • Owned types configured with OwnsOne or discovered through a reference navigation always have a one-to-one relationship with the owner, therefore they don't need their own key values as the foreign key values are unique. – Pedro Coelho Feb 25 '20 at 22:53
  • Hi again, thank you for your answers. However, there is no "ValueObject" class, and I'm not using inheritance at all. As for the exception, I already knew the problem, I wrote it in my initial message. My question is not about the exception being thrown, but how to correctly implement the null-object pattern in a way that EFCore is happy about it. Thanks again! – Fernando Gómez Feb 25 '20 at 23:15
  • @FernandoGómez You are applying value object concept in a weird way – Pedro Coelho Feb 25 '20 at 23:19
  • I'm using value object concept the way Martin Fowler described it[1]: an inmutable object (getters but no setters, or only private setters) with no identity at all. What's weird about that? Also the post is not about value objects, but rather about the null-object pattern and ef-core (hit: read the title). [1] https://martinfowler.com/bliki/ValueObject.html – Fernando Gómez Feb 25 '20 at 23:26