EF Core - Create composite unique index with a value object and a parent type
Asked Answered
B

2

8

I have an entity with an ExternalSystemName value object and a Deployment parent type which is another entity. The important part of the model looks like this :

public sealed class ExternalSystem : Entity
{
    public ExternalSystemName Name { get; private set; }

    public Deployment Deployment { get; private set; }
}

The uniqueness of this entity is determined by a combination of the deployment ID (stored in the deployment entity class) and the name (which is the value of the ExternalSystemName value object). In other words, a deployment cannot have 2 external systems with the same name.

I am facing an issue when trying to setup this combined unique index with an IEntityTypeConfiguration implementation :

internal sealed class ExternalSystemsConfiguration : 
IEntityTypeConfiguration<ExternalSystem>
{
    public void Configure(EntityTypeBuilder<ExternalSystem> builder)
    {
        builder.ToTable("TblExternalSystems");

        builder.OwnsOne(e => e.Name, navigationBuilder =>
        {
            navigationBuilder.Property(e => e.Value)
            .HasColumnName("Name");
        });

        builder.HasIndex(e => new { e.Name, e.Deployment }).IsUnique();
    }
}

I am getting this exception when running my API :

System.InvalidOperationException: ''Name' cannot be used as a property on entity type 'ExternalSystem' because it is configured as a navigation.'

I tried pointing the index to e.Name.Value instead and I am getting this error :

System.ArgumentException: 'The expression 'e => new <>f__AnonymousType0`2(Value = e.Name.Value, Deployment = e.Deployment)' is not a valid member access expression. The expression should represent a simple property or field access: 't => t.MyProperty'. When specifying multiple properties or fields, use an anonymous type: 't => new { t.MyProperty, t.MyField }'. (Parameter 'memberAccessExpression')'

I also tried a unique index on just one of these properties and I get the navigation error regardless. I fear I know the answer already but does this mean EF Core only supports indexes on columns that are not a non-entity, non-valueObject type? Does that mean my model needs to have a Guid property representing the Deployment ID instead of having the Deployment itself?

UPDATE

I learned that EF Core can deal with reference / primitive pairs just fine. With that in mind, my ExternalSystem entity can now have BOTH these properties :

public Deployment Deployment { get; private set; }

public Guid DeploymentId { get; private set; }

That Guid property is not part of the constructor and because they ultimately get the same column name everything works fine. I can now just add this to my configuration for this entity and the index is created properly :

builder.HasIndex(e => new { e.DeploymentId}).IsUnique();

My issue is now with the value object. Using the same approach, I suppose I could do something like this ?

public ExternalSystemName NameV { get; private set; }

public string Name { get; private set; }

I have to rename the value object property since they obviously can't share the same name. This is not something I had to do with the entity type since EF Core knew to add "Id" to the column name in the first place. With this setup, EF Core is duplicating the columns. One has the name "Name" and the other one has "ExternalSystem_Name". Obviously everthing else fails from there since that column doesn't accept null values. Why is this happening?

Bulley answered 28/10, 2022 at 20:34 Comment(4)
Add properties NameId and DeploymentId to ExternalSystem and add an index on those properties.Demure
so those Id properties could live in parallel with the associated entity type and play well with the underlying DB?Bulley
Yes, the underlying db should already have these columns as FKs and EF knows how to deal with reference/primitive property pairs (aka foreign key associations).Demure
That works for the entity type but I'm not sure how to then tackle the value object in the same approach. I will update my original post.Bulley
B
2

I fixed this by defining a converter for the ExternalSystemName property :

builder.Property(e => e.Name)
        .HasConversion(
            e => e.Value,
            v => ExternalSystemName.Create(v).Value);

I can now declare my unique index as such :

    builder.HasIndex(e => new { e.DeploymentId, e.Name}).IsUnique();

I also upgraded to .NET 7 and EF Core 7 since asking this question so it may have been a factor as well.

Bulley answered 23/11, 2022 at 19:2 Comment(0)
P
-1

You need to manually define the key to be the foreign key using Fluent api.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ExternalSystem>()
       .HasOne(g => g.NameV)
       .WithOne()
       .HasForeignKey<ExternalSystem>(j => j.Name);
 }

Put this method inside your DB context class

Peterson answered 4/11, 2022 at 3:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.