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

  1. Mark every navigation property as virtual so both getter and setter are overridable: public virtual ICollection<Post> Posts { get; set; }.
  2. If the setter cannot be virtual, do not use UseChangeTrackingProxies for that entity.
  3. 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

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


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