Search code examples
c#entity-framework-corevalue-objects

Using C# 9 records as properties in entities with Entity Framework Core


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?


Solution

  • 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.