Skip to content
Merged
38 changes: 38 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,41 @@ 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 `private` (if they don't have any accessibility modifiers)
and `sealed` (if they don't have the `private` modifier which implies `sealed`, and they don't already have modifier `virtual` or `sealed`).

```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; // implements I.P
}
```

```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