1

I want to use value objects as properties in my project (in my project value objects are C# 9 record types).

The entity looks like this:

public class Client : IEntity
{
    public int Id { get; set; }
    public ClientId ClientId { get; set; }
}

And ClientId value object:

public record ClientId
{
    private readonly byte[] _bytes;

    public ClientId(byte[] bytes)
    {
        if (bytes is null || bytes.Length != 32)
            throw new ArgumentException($"'{nameof(bytes)}' must be 32 bytes long");

        _bytes = bytes;
    }

    public string Value => Base64UrlEncoder.Encode(_bytes);
}

When I do migration I get an following error:

No suitable constructor was found for entity type 'ClientId'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'bytes' in 'ClientId(byte[] bytes)'; cannot bind 'original' in 'ClientId(ClientId original)'.

I know that this error occurs because I don't have empty constructor, but I really don't want to have it because I want to validate the length of given _bytes. What's more, even when I have added this empty constructor:

public record ClientId
{
    private readonly byte[] _bytes;

    public ClientId()
    {
    }

    public ClientId(byte[] bytes)
    {
        if (bytes is null || bytes.Length != 32)
            throw new ArgumentException($"'{nameof(bytes)}' must be 32 bytes long");

        _bytes = bytes;
    }

    public string Value => Base64UrlEncoder.Encode(_bytes);
}

I get the error:

The entity type 'ClientId' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.

It seems to me that EF Core treats the record type as another entity and wants to create a relationship.

What am I doing wrong?

Szyszka947
  • 473
  • 2
  • 5
  • 21
  • Can you post the code you tried with a parameterless constructor? – Steve V Oct 27 '22 at 12:25
  • Have you tried to make default constructor `private`? – Svyatoslav Danyliv Oct 27 '22 at 13:34
  • Yes, it didn't change anything. I still get error that says I don't have primary key. – Szyszka947 Oct 27 '22 at 13:54
  • Where is `bytes` coming from? You should add model building code. EF is trying to understand the constructor by mapping that value to a property, but there's no public property. You shouldn't be getting an error about primary key, EF should be mapping the record properties back to the parent `IEntity` you defined. What version of EFCore are you using? (And C#9 or 10?) – BurnsBA Oct 27 '22 at 14:01
  • `_bytes` is just used to set `Value` basing on it, during creating `ClientId`. My `IEntity` is just empty interface which is used only for generic types and methods. I use NET 6, C# 10 and latest version of EFCore. – Szyszka947 Oct 27 '22 at 14:14
  • Yes, you already showed how `bytes` should be used in your question, but still haven't explained where the value comes from or how EF knows how to map that into a class. – BurnsBA Oct 27 '22 at 14:33
  • So EFCore will not automatically create appropriate columns basing on record? How can I tell EFCore how to map value objects to columns? – Szyszka947 Oct 27 '22 at 14:42

1 Answers1

1

You have to use Value Conversions.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Client>()
        .Property(e => e.ClientId)
        .HasConversion(
            v => v.Value,
            v => new ClientId(Base64UrlEncoder.DecodeBytes(v)));
}

In this case, the default constructor is not needed.

Alexander Petrov
  • 13,457
  • 2
  • 20
  • 49
  • Good solution, but what if I have value object with more than one property? E.g. `record Lifetime` with properties `int Seconds { get; set; }` and `int Minutes { get; set; }`? How can I do value convertion for such `record`? – Szyszka947 Nov 16 '22 at 12:05
  • 1
    On the same page linked by @alexander-petrov look at the [composite value objects](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions?tabs=data-annotations#composite-value-objects) section that addresses your question. In such a case, you cannot use value conversions, since they are limited to a single column. You'd have to serialize/deserialize the object. – Phylyp Nov 20 '22 at 17:44