dotnet/efcore · error · InvalidOperationException
Property '{1_entityType}.{0_property}' is not virtual. 'UseC
Error message
Property '{1_entityType}.{0_property}' 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
When UseChangeTrackingProxies is enabled, ProxyBindingRewriter checks each navigation's PropertyInfo setter. If the setter exists but is not virtual (SetMethod?.IsReallyVirtual() == false), it throws NonVirtualProperty. Change-tracking proxies must override the setter to intercept writes, which requires the setter to be virtual. The resource string uses numbered format parameters ({1_entityType}.{0_property}) to protect entity type names when sensitive logging is off.
Source
Thrown at src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs:92
throw new InvalidOperationException(ProxiesStrings.ItsASeal(entityType.DisplayName()));
}
foreach (var navigationBase in entityType.GetDeclaredNavigations()
.Concat<IConventionNavigationBase>(entityType.GetDeclaredSkipNavigations()))
{
if (!navigationBase.IsShadowProperty())
{
if (_options.UseChangeTrackingProxies)
{
if (navigationBase.PropertyInfo == null)
{
throw new InvalidOperationException(
ProxiesStrings.FieldProperty(navigationBase.Name, entityType.DisplayName()));
}
if (navigationBase.PropertyInfo.SetMethod?.IsReallyVirtual() == false)
{
throw new InvalidOperationException(
ProxiesStrings.NonVirtualProperty(navigationBase.Name, entityType.DisplayName()));
}
}
if (_options.UseLazyLoadingProxies
&& navigationBase.LazyLoadingEnabled)
{
if (navigationBase.PropertyInfo == null
|| !navigationBase.PropertyInfo.GetMethod!.IsReallyVirtual())
{
if (!_options.IgnoreNonVirtualNavigations
&& navigationBase is not INavigation { ForeignKey.IsOwnership: true })
{
if (navigationBase.PropertyInfo == null)
{
throw new InvalidOperationException(
ProxiesStrings.FieldProperty(navigationBase.Name, entityType.DisplayName()));
}View on GitHub (pinned to 3a2006ef56)
Solutions
- Mark every navigation property as virtual so both getter and setter are overridable: public virtual ICollection<Post> Posts { get; set; }.
- If the setter cannot be virtual, do not use UseChangeTrackingProxies for that entity.
- Ensure all properties (not just navigations) are virtual when using change-tracking proxies.
Example fix
// before — non-virtual navigation setter
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; set; }
}
// UseChangeTrackingProxies() → throws NonVirtualProperty
// after — mark navigation virtual
public class Blog
{
public int Id { get; set; }
public virtual ICollection<Post> Posts { get; set; }
} Defensive patterns
Strategy: validation
Validate before calling
// At startup, verify all navigation setters are virtual for change-tracking proxies.
static void ValidateVirtualNavigationSetters(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var nav in entityType.GetDeclaredNavigations()
.Concat<INavigationBase>(entityType.GetDeclaredSkipNavigations()))
{
if (!nav.IsShadowProperty()
&& nav.PropertyInfo?.SetMethod?.IsVirtual != true)
{
throw new InvalidOperationException(
$"{entityType.DisplayName()}.{nav.Name} setter must be virtual.");
}
}
}
} Type guard
// Type guard: check if a property has a virtual setter
static bool HasVirtualSetter(PropertyInfo? prop)
=> prop?.SetMethod?.IsVirtual == true && !prop.SetMethod.IsFinal; Try / catch
try
{
using var context = new MyContext(options);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("not virtual"))
{
// Mark the navigation property virtual and retry
logger.LogError(ex, "Navigation setter not virtual; mark it virtual for change-tracking proxies.");
throw;
} Prevention
- Mark all navigation properties as virtual (public virtual ICollection<T> Nav { get; set; }).
- Add a startup model validation pass that checks navigation setters are virtual.
- Establish a project convention that all entity properties are virtual when proxies are used.
- Review auto-generated entity classes for missing virtual modifiers.
When it happens
Trigger: Enabling UseChangeTrackingProxies() when at least one navigation property has a non-virtual setter (i.e. the property itself is not virtual or only the getter is virtual). The throw occurs during model finalization when ProxyBindingRewriter validates navigation setters.
Common situations: Entity classes with auto-properties that are not marked virtual (public ICollection<Post> Posts { get; set; }). Using read-only collection properties with a non-virtual private setter. Applying change-tracking proxies to an existing model where properties were not designed for proxy overriding.
Related errors
- Property '{property}' on entity type '{entityType}' is mappe
- The mapped indexer property on entity type '{entityType}' is
- The type '{dictionaryType}' used for shared entity type '{en
- Entity type '{entityType}' is sealed. 'UseChangeTrackingProx
- 'UseChangeTrackingProxies' and 'UseLazyLoadingProxies' each
AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11).
Data as JSON: /api/errors/2ca6436534a66d7a.
Report an issue: GitHub.