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
- Make the indexer property virtual (both getter and setter) on your custom CLR type so the proxy can override it.
- If the indexer cannot be virtual, do not enable UseChangeTrackingProxies for entities using that CLR type.
- 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
- Make indexer properties virtual (both getter and setter) on custom entity CLR types.
- Avoid indexer-based property mapping when change-tracking proxies are enabled unless the indexer is virtual.
- Add a startup model validation pass that checks indexer properties are virtual.
- Use explicit CLR properties instead of indexers when proxy interception is required.
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
- Property ' . ' is not virtual. 'UseChangeTrackingProxies'…
- The type ' ' used for shared entity type ' ' is not…
- Property ' ' on entity type ' ' is mapped without a CLR…
- Entity type ' ' is sealed. 'UseChangeTrackingProxies'…
- The entity type ' ' was not found. Ensure that the entity…
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)