Search code examples
c#entity-framework-coreunique-constraintvalue-objects

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


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?


Solution

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