dotnet/efcore · error · InvalidOperationException

The mapped indexer property on entity type

Error message

The mapped indexer property on entity type '{entityType}' is not virtual. 'UseChangeTrackingProxies' requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. 'UseLazyLoadingProxies' requires only the navigation properties be virtual.

What it means

ProxyBindingRewriter checks the indexer property of entity types when UseChangeTrackingProxies is enabled. If the indexer's setter is not virtual and the CLR type is NOT a Dictionary<string, T> (i.e. a custom indexer type), it throws NonVirtualIndexerProperty. Change-tracking proxies must override the indexer setter to intercept writes to indexer-mapped properties, which requires the indexer to be virtual. The else-branch covers all indexer-bearing types that are not Dictionary<,>.

Solutions

  1. Make the indexer property virtual (both getter and setter) on your custom CLR type so the proxy can override it.
  2. If the indexer cannot be virtual, do not enable UseChangeTrackingProxies for entities using that CLR type.
  3. Use a different property mapping strategy (explicit CLR properties) instead of indexer properties when proxies are required.

Example fix

// before — non-virtual indexer
public class DynamicEntity
{
    public object this[string key]
    {
        get => _data[key];
        set => _data[key] = value;
    }
    private readonly Dictionary<string, object> _data = new();
}
// UseChangeTrackingProxies() → throws NonVirtualIndexerProperty

// after — virtual indexer
public class DynamicEntity
{
    public virtual object this[string key]
    {
        get => _data[key];
        set => _data[key] = value;
    }
    private readonly Dictionary<string, object> _data = new();
}
Defensive patterns

Strategy: validation

Validate before calling

// At startup, verify indexer properties on entity types are virtual
// when change-tracking proxies are enabled.
static void ValidateVirtualIndexers(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var prop in entityType.GetDeclaredProperties().Where(p => !p.IsShadowProperty()))
        {
            if (prop.IsIndexerProperty()
                && prop.PropertyInfo?.SetMethod?.IsVirtual != true)
            {
                throw new InvalidOperationException(
                    $"{entityType.DisplayName()} indexer must be virtual for change-tracking proxies.");
            }
        }
    }
}

Type guard

// Type guard: check if a type has a virtual indexer suitable for proxying
static bool HasVirtualIndexer(Type clrType)
{
    foreach (var prop in clrType.GetProperties())
    {
        if (prop.GetIndexParameters().Length > 0
            && (prop.SetMethod?.IsVirtual != true || prop.SetMethod.IsFinal))
            return false;
    }
    return true;
}

Try / catch

try
{
    using var context = new MyContext(options);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("indexer property") && ex.Message.Contains("not virtual"))
{
    // Make the indexer virtual on the custom CLR type, or disable change-tracking proxies
    logger.LogError(ex, "Indexer not virtual; make it virtual for change-tracking proxies.");
    throw;
}

Prevention

When it happens

Trigger: Using an entity CLR type that has an indexer property (e.g. a custom dynamic bag class with this[string key]) with indexer-mapped properties, while UseChangeTrackingProxies is enabled and the indexer setter is non-virtual. The throw occurs during model finalization.

Common situations: Custom dynamic/expando entity types with indexers for flexible property mapping. Shared-type entities using a custom collection-like CLR type with an indexer instead of Dictionary. Combining indexer-based property mapping with change-tracking proxies.

Related errors


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

Appendix: source

Thrown at src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs:183

                                if (!property.PropertyInfo!.SetMethod!.IsReallyVirtual())
                                {
                                    if (clrType.IsGenericType
                                        && clrType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
                                        && clrType.GenericTypeArguments[0] == typeof(string))
                                    {
                                        if (entityType.GetProperties().Any(p => !p.IsPrimaryKey()))
                                        {
                                            throw new InvalidOperationException(
                                                ProxiesStrings.DictionaryCannotBeProxied(
                                                    clrType.ShortDisplayName(),
                                                    entityType.DisplayName(),
                                                    typeof(IDictionary<,>).MakeGenericType(clrType.GenericTypeArguments)
                                                        .ShortDisplayName()));
                                        }
                                    }
                                    else
                                    {
                                        throw new InvalidOperationException(
                                            ProxiesStrings.NonVirtualIndexerProperty(entityType.DisplayName()));
                                    }
                                }
                            }
                        }
                        else
                        {
                            if (property.PropertyInfo == null)
                            {
                                throw new InvalidOperationException(
                                    ProxiesStrings.FieldProperty(property.Name, entityType.DisplayName()));
                            }

                            if (property.PropertyInfo.SetMethod?.IsReallyVirtual() == false)
                            {
                                throw new InvalidOperationException(
                                    ProxiesStrings.NonVirtualProperty(property.Name, entityType.DisplayName()));
                            }

View on GitHub (pinned to 3a2006ef56)