dotnet/efcore · error · InvalidOperationException

The specified discriminator value

Error message

The specified discriminator value '{value}' for '{entityType}' is not a string. Configure a string discriminator value instead.

What it means

Thrown inside ValidateDiscriminatorValues (the non-TPH branch) when a concrete derived type's discriminator value, returned by GetDiscriminatorValue(), cannot be cast to string. In TPT/TPC hierarchies the discriminator value defaults to the entity's short name (a string), so any explicitly-set non-string discriminator value is rejected.

Solutions

  1. Set a string discriminator value instead: SetDiscriminatorValue("Customer").
  2. Remove the custom discriminator value and let EF use the entity short name automatically.
  3. If you need numeric discriminator semantics, stay on TPH mapping where non-string discriminator values are supported.

Example fix

// before (TPT hierarchy)
modelBuilder.Entity<Customer>().HasBaseType<Person>()
    .Property("Discriminator").HasConversion<string>();
modelBuilder.Entity<Customer>().Metadata.SetDiscriminatorValue(2); // int -> throws 422

// after
modelBuilder.Entity<Customer>().Metadata.SetDiscriminatorValue("Customer");
Defensive patterns

Strategy: validation

Validate before calling

foreach (var entityType in modelBuilder.Model.GetEntityTypes()
    .Where(t => t.BaseType != null))
{
    var root = entityType.GetRootType();
    var strategy = root.GetMappingStrategy();
    if (strategy != RelationalAnnotationNames.TphMappingStrategy
        && strategy != null)
    {
        var dv = entityType.GetDiscriminatorValue();
        if (entityType.ClrType.IsInstantiable() && dv is not null and not string)
        {
            throw new InvalidOperationException($"{entityType.DisplayName()} has non-string discriminator value {dv}; set a string.");
        }
    }
}

Prevention

When it happens

Trigger: In a TPT or TPC hierarchy, calling SetDiscriminatorValue(42) or SetDiscriminatorValue(true) (any non-string), or applying a [Discriminator] with an int/enum/bool value on a derived type. Only abstract/non-instantiable types are skipped; concrete types are checked.

Common situations: Porting a TPH model that used integer discriminator values into a TPT/TPC hierarchy without changing them; assuming discriminator values are type-flexible across all strategies.

Related errors


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

Appendix: source

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

            var derivedTypes = entityType.GetDerivedTypesInclusive().ToList();
            var discriminatorValues = new Dictionary<string, IEntityType>();
            foreach (var derivedType in derivedTypes)
            {
                foreach (var complexProperty in derivedType.GetDeclaredComplexProperties())
                {
                    ValidateDiscriminatorValues(complexProperty.ComplexType);
                }

                var discriminatorValue = derivedType.GetDiscriminatorValue();
                if (!derivedType.ClrType.IsInstantiable()
                    || discriminatorValue is null)
                {
                    continue;
                }

                if (discriminatorValue is not string valueString)
                {
                    throw new InvalidOperationException(
                        RelationalStrings.NonTphDiscriminatorValueNotString(discriminatorValue, derivedType.DisplayName()));
                }

                if (discriminatorValues.TryGetValue(valueString, out var duplicateEntityType))
                {
                    throw new InvalidOperationException(
                        RelationalStrings.EntityShortNameNotUnique(
                            derivedType.Name, discriminatorValue, duplicateEntityType.Name));
                }

                discriminatorValues[valueString] = derivedType;
            }
        }
    }

    /// <summary>
    ///     Validates the key value generation is valid.
    /// </summary>

View on GitHub (pinned to 3a2006ef56)