dotnet/efcore · error · InvalidOperationException

Entity type ' ' is an optional dependent using table…

Error message

Entity type '{entityType}' is an optional dependent using table sharing and containing other dependents without any required non shared property to identify whether the entity exists. If all nullable properties contain a 'null' value in database then an object instance won't be created in the query causing nested dependent's values to be lost. Add a required property to create instances with 'null' values for other properties or mark the incoming navigation as required to always create an instance.

What it means

Thrown by ValidateOptionalDependents when an optional dependent (owned type, nullable navigation) is shared-table-mapped with the principal AND itself has referencing foreign keys whose declaring entity types are also in mappedTypes — i.e. nested owned dependents — yet the optional dependent has no required non-shared identifying column. Without such a column, EF cannot distinguish a null instance from an all-null instance, and would silently lose nested dependents during query, so the validator throws rather than corrupt data.

Solutions

  1. Add at least one required (non-nullable, non-shared) property to the optional dependent so EF can detect its existence: builder.OwnsOne(e => e.Address, a => { a.Property(x => x.Street).IsRequired(); }).
  2. Mark the navigation to the dependent as required: builder.OwnsOne(e => e.Address, a => a.Navigation(e => e.Address).IsRequired()) — but then it is no longer optional.
  3. Restructure so the nested dependents live on a separate table or are not mapped to the same shared table.

Example fix

// before
builder.OwnsOne(e => e.Address, a =>
{
    a.ToTable("Customers");
    a.Property(x => x.Street);  // nullable
    a.OwnsOne(x => x.Geo, g => g.Property(p => p.Lat)); // nested dependent
});

// after
builder.OwnsOne(e => e.Address, a =>
{
    a.ToTable("Customers");
    a.Property(x => x.Street).IsRequired(); // sentinel for existence
    a.OwnsOne(x => x.Geo, g => g.Property(p => p.Lat));
});
Defensive patterns

Strategy: validation

Validate before calling

// Walk owned types and warn on optional dependents with nested dependents but no sentinel.
foreach (var et in model.GetEntityTypes())
{
    var owns = et.GetReferencingForeignKeys()
        .Where(fk => fk.IsOwnership && fk.DeclaringEntityType.IsMappedToTable());
    foreach (var fk in owns)
    {
        var owned = fk.DeclaringEntityType;
        bool navigationOptional = fk.Properties.Any(p => p.IsNullable);
        bool hasNestedDependent = owned.GetReferencingForeignKeys()
            .Any(f => f.IsOwnership);
        bool hasRequiredNonShared = owned.GetProperties()
            .Any(p => !p.IsNullable && !p.IsShadowProperty());
        if (navigationOptional && hasNestedDependent && !hasRequiredNonShared)
            Console.WriteLine($"{owned.Name} is an optional dependent with nested dependents and no sentinel property.");
    }
}

Prevention

When it happens

Trigger: An owned entity type configured with .OwnsOne(...) where the navigation is optional (nullable), sharing the principal's table, and that owned type itself has .OwnsOne(...) children mapped to the same table, but every column on the optional dependent is nullable (so all-null is a valid row). The validator's requiredNonSharedColumnFound flag stays false AND GetReferencingForeignKeys().Any(... mappedTypes.Contains) is true -> throw.

Common situations: Optional owned address with optional owned contact-info nested in it, all columns nullable; refactoring an entity to owned without marking a sentinel required column; TPH with optional complex types.

Related errors


AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11). Data as JSON: /api/errors/b9f36265e8d4ba2e. Report an issue: GitHub.

Appendix: source

Thrown at src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs:1102

                    continue;
                }

                var columnName = property.GetColumnName(tableIdentifier);
                if (columnName != null)
                {
                    if (!principalColumns.Contains(columnName))
                    {
                        requiredNonSharedColumnFound = true;
                        break;
                    }
                }
            }

            if (!requiredNonSharedColumnFound)
            {
                if (entityType.GetReferencingForeignKeys().Select(e => e.DeclaringEntityType).Any(t => mappedTypes.Contains(t)))
                {
                    throw new InvalidOperationException(
                        RelationalStrings.OptionalDependentWithDependentWithoutIdentifyingProperty(entityType.DisplayName()));
                }

                logger.OptionalDependentWithoutIdentifyingPropertyWarning(entityType);
            }
        }

        (List<IEntityType> EntityTypes, bool Optional) GetPrincipalEntityTypes(IEntityType entityType)
        {
            if (!principalEntityTypesMap.TryGetValue(entityType, out var tuple))
            {
                var list = new List<IEntityType>();
                var optional = false;
                foreach (var foreignKey in entityType.FindForeignKeys(entityType.FindPrimaryKey()!.Properties))
                {
                    var principalEntityType = foreignKey.PrincipalEntityType;
                    if (foreignKey.PrincipalEntityType.IsAssignableFrom(foreignKey.DeclaringEntityType)
                        || !mappedTypes.Contains(principalEntityType))

View on GitHub (pinned to 3a2006ef56)