Skip to content

Throw for change-tracking proxy properties whose setters cannot be overridden - #39592

Draft
ajcvickers wants to merge 2 commits into
dotnet:mainfrom
ajcvickers:fix-39589-internal-proxy-properties
Draft

ajcvickers wants to merge 2 commits into
dotnet:mainfrom
ajcvickers:fix-39589-internal-proxy-properties

Conversation

@ajcvickers

Copy link
Copy Markdown
Contributor

Fixes #39589

Draft: stacked on #39591. GitHub can't base this PR on another fork branch, so it targets main and temporarily includes the commit from #39591 (Validate that proxies can override navigation accessors). Only the second commit, Validate that change-tracking proxies can override property setters, is new here. I'll rebase onto main once #39591 merges, and the diff will then shrink to that commit.

Change-tracking proxies must override the setter of every mapped property to receive change notifications. A property whose setter is not visible to DynamicProxyGenAssembly2, such as public virtual string Name { get; internal set; }, passed model validation but was silently never tracked, so changes to it were not detected or saved. #39591 fixes the same problem for navigations.

Change

Model validation (ProxyBindingRewriter) now throws InvalidOperationException when UseChangeTrackingProxies is used and the setter of

  • a mapped scalar property, or
  • the mapped indexer property

cannot be overridden by the proxy. It uses the same Castle accessibility rules as #39591 (ProxyUtil.IsAccessible), so InternalsVisibleTo("DynamicProxyGenAssembly2") is honored. The scalar message is the one added in #39591 and names the property; the indexer has a new message, since the CLR indexer has no useful name.

  • Key properties and properties on owned types are validated like any other property.
  • Lazy-loading proxies are unaffected: they don't intercept scalar properties.
  • Getter-only properties are still skipped, as for the existing virtual check.

Tests

  • ProxyAccessibilityTests (emitted types): scalar properties and the indexer at every accessibility level, with and without InternalsVisibleTo; lazy-loading does not validate them; and an end-to-end test that a visible internal setter is intercepted, so the entity becomes Modified without DetectChanges.
  • ChangeDetectionProxyTests (plain C# classes): { get; internal set; } on a property and on a key throws, and { get; protected set; } is accepted.

Verified with the Proxies tests (160), the InMemory, Sqlite and SQL Server proxy functional tests, and the AspNet identity and complex-type tracking tests. All pass.

Breaking change

Apps with public virtual T Prop { get; internal set; } (including keys) under change-tracking proxies, without InternalsVisibleTo("DynamicProxyGenAssembly2"), will now get a model-validation error instead of changes being silently lost. The docs breaking-change note (dotnet/EntityFramework.Docs#5515) is being extended to cover this.

Not in this PR

Entity types that are abstract are skipped by all of the rewriter's checks, so an abstract base with an invisible setter is still not validated. This is an existing gap that also applies to navigations and the virtual check.

🤖 Generated with Claude Code

ajcvickers and others added 2 commits November 10, 2026 11:40
Castle proxies cannot override members that are not visible to
DynamicProxyGenAssembly2, such as internal virtual navigations, unless the
declaring assembly has [InternalsVisibleTo("DynamicProxyGenAssembly2")].
Such navigations passed model validation but were silently never
lazy-loaded or change-tracked.

Model validation now throws when the getter (lazy-loading proxies) or the
setter (change-tracking proxies) of a navigation cannot be overridden,
using Castle's own accessibility rules. IgnoreNonVirtualNavigations and
EnableLazyLoading(false) still suppress the check for lazy-loading.
Entity types that are themselves inaccessible are not reported here, since
they already fail when a proxy is created.

Fixes dotnet#32352

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Change-tracking proxies must override the setter of every mapped property
to receive change notifications. A property whose setter is not visible to
DynamicProxyGenAssembly2, such as `public virtual string Name { get; internal set; }`,
passed model validation but was silently never tracked, so changes to it
were not detected or saved.

Model validation now throws when the setter of a mapped scalar property or
of the mapped indexer property cannot be overridden by the proxy, using the
same Castle accessibility rules as for navigations. Key properties are
validated like any other property. Lazy-loading proxies are unaffected.

Fixes dotnet#39589

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Change-tracking proxies silently ignore internal virtual properties

1 participant