dotnet/efcore · error · InvalidOperationException

Property '{entityType}.{property}' is not virtual. 'UseChang

Error message

Property '{entityType}.{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

With UseChangeTrackingProxies enabled, a non-shadow navigation whose setter is not virtual throws NonVirtualProperty (ProxyBindingRewriter.cs:90-94). The proxy must override the setter to intercept change notifications.

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 dbf9771522)

Solutions

  1. Make the navigation property virtual (so both getter and setter can be overridden by the proxy).
  2. Stop using change-tracking proxies for that entity type.

Example fix

// before
public ICollection<Post> Posts { get; set; } = new List<Post>();
// after
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
Defensive patterns

Strategy: validation

Validate before calling

// Verify navigation setters are virtual before enabling change-tracking proxies:
foreach (var et in modelBuilder.Model.GetEntityTypes())
foreach (var nav in et.GetNavigations())
{
    var pi = nav.PropertyInfo;
    if (pi is not null && pi.SetMethod?.IsVirtual != true)
        throw new InvalidOperationException($"{et.Name}.{nav.Name} setter must be virtual.");
}

Type guard

static bool IsVirtualSetter(System.Reflection.PropertyInfo? pi)
    => pi?.SetMethod?.IsVirtual == true;

Prevention

When it happens

Trigger: Enabling UseChangeTrackingProxies when a navigation property's setter is non-virtual.

Common situations: Applying change-tracking proxies to an existing model where navigations were written as plain auto-properties.

Related errors


AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06). Data as JSON: /api/errors/57bc65d515b6b013. Report an issue: GitHub.