Skip to content
Merged
37 changes: 37 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,40 @@ class extension { } // type may not be named "extension"
class C<extension> { } // type parameter may not be named "extension"
```

## Partial properties and events are now implicitly virtual and public

***Introduced in Visual Studio 2022 version 17.15***

We have fixed [an inconsistency](https://github.com/dotnet/roslyn/issues/77346)
where partial interface properties and events would not be implicitly `virtual` and `public` unlike their non-partial equivalents.
This inconsistency is however [preserved](./Deviations%20from%20Standard.md#interface-partial-methods) for partial interface methods to avoid a larger breaking change.
Note that Visual Basic and other languages not supporting default interface members will start requiring to implement implicitly virtual `partial` interface members.

To keep the previous behavior, explicitly mark `partial` interface members as `sealed` and `private`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: private by itself should accomplish this, right? Since the member will be implicitly sealed in that case. It looks like private sealed is permitted on static interface methods but not instance ones.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private will be enough for members like partial int P { get; }. But if you had a member like public partial int P { get; }, the workaround to preserve previous behavior is adding sealed modifier (you cannot add private since the member is already public). Let me try to clarify, thanks.


```cs
System.Console.Write(((I)new C()).P); // wrote 1 previously, writes 2 now

partial interface I
{
public partial int P { get; }
public partial int P => 1; // implicitly virtual now
}

class C : I
{
public int P => 2; // overrides I.M
}
```

```cs
System.Console.Write(((I)new C()).P); // inaccessible previously, writes 1 now

partial interface I
{
partial int P { get; } // implicitly public now
partial int P => 1;
}

class C : I;
```
7 changes: 7 additions & 0 deletions docs/compilers/CSharp/Deviations from Standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ The compiler is free to make assumptions about the shape and behavior of well-kn
It may not check for unexpected constraints, `Obsolete` attribute, or `UnmanagedCallersOnly` attribute.
It may perform some optimizations based on expectations that the types/members are well-behaved.
Note: the compiler should remain resilient to missing well-known types/members.

# Interface partial methods

Interface partial methods are implicitly non-virtual,
unlike non-partial interface methods and other interface partial member kinds,
see [a related breaking change](./Compiler%20Breaking%20Changes%20-%20DotNet%2010.md#partial-properties-and-events-are-now-implicitly-virtual-and-public)
and [LDM 2025-04-07](https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-04-07.md#breaking-change-discussion-making-partial-members-in-interfaces-virtual-andor-public).
34 changes: 19 additions & 15 deletions src/Compilers/CSharp/Portable/Symbols/Source/ModifierUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,24 @@ internal static void CheckFeatureAvailabilityForPartialEventsAndConstructors(Loc
}
#nullable disable

internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation)
internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(DeclarationModifiers mods, bool hasBody, bool isExplicitInterfaceImplementation, bool forMethod)
{
// Interface partial non-method members are implicitly public and virtual just like their non-partial counterparts.
// Interface partial methods are implicitly private and not virtual (this is a spec violation but being preserved to avoid breaks).
bool notPartialOrNewPartialBehavior = (mods & DeclarationModifiers.Partial) == 0 || !forMethod;

if ((mods & DeclarationModifiers.AccessibilityMask) == 0)
{
if (!isExplicitInterfaceImplementation && notPartialOrNewPartialBehavior)
{
mods |= DeclarationModifiers.Public;
}
else
{
mods |= DeclarationModifiers.Private;
}
}

if (isExplicitInterfaceImplementation)
{
if ((mods & DeclarationModifiers.Abstract) != 0)
Expand All @@ -258,11 +274,11 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
{
mods &= ~DeclarationModifiers.Sealed;
}
else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Partial | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0)
else if ((mods & (DeclarationModifiers.Private | DeclarationModifiers.Virtual | DeclarationModifiers.Abstract)) == 0 && notPartialOrNewPartialBehavior)
{
Debug.Assert(!isExplicitInterfaceImplementation);

if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Sealed)) != 0)
if (hasBody || (mods & (DeclarationModifiers.Extern | DeclarationModifiers.Partial | DeclarationModifiers.Sealed)) != 0)
{
if ((mods & DeclarationModifiers.Sealed) == 0)
{
Expand All @@ -279,18 +295,6 @@ internal static DeclarationModifiers AdjustModifiersForAnInterfaceMember(Declara
}
}

if ((mods & DeclarationModifiers.AccessibilityMask) == 0)
{
if ((mods & DeclarationModifiers.Partial) == 0 && !isExplicitInterfaceImplementation)
{
mods |= DeclarationModifiers.Public;
}
else
{
mods |= DeclarationModifiers.Private;
}
}

return mods;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli
// Proper errors must have been reported by now.
if (isInterface)
{
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation);
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, !isFieldLike, explicitInterfaceImplementation, forMethod: false);
}

return mods;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,8 @@ private static DeclarationModifiers AddImpliedModifiers(DeclarationModifiers mod
if (containingTypeIsInterface)
{
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, hasBody,
methodKind == MethodKind.ExplicitInterfaceImplementation);
methodKind == MethodKind.ExplicitInterfaceImplementation,
forMethod: true);
}
else if (methodKind == MethodKind.ExplicitInterfaceImplementation)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ private static (DeclarationModifiers modifiers, bool hasExplicitAccessMod) MakeM
// Proper errors must have been reported by now.
if (isInterface)
{
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation);
mods = ModifierUtils.AdjustModifiersForAnInterfaceMember(mods, accessorsHaveImplementation, isExplicitInterfaceImplementation, forMethod: false);
}

if (isIndexer)
Expand Down
Loading
Loading