Modern C# · Lesson 43

C# 14 partial constructors and events

Compare one complete type with defining and implementing declarations.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The concept and environment

C# 14 adds partial instance constructors and partial events. These are compile-time declarations split into defining and implementing parts of the same partial type. The external Meter project includes separate declaration and implementation files to demonstrate the real feature. The portable .NET 9 companion supplies one ordinary complete class with equivalent construction and event behavior. It is a logic comparison, not a partial-member processor. A partial type is assembled by the compiler; it is not an object whose missing methods are dynamically loaded the first time an event happens.

Walk through the portable program

The meter begins at two. A retained observer delegate is subscribed, then Add of three updates Value to five and synchronously invokes the stored handlers, printing changed:5. Removing that exact observer delegate detaches it. The next Add increases Value to six but produces no event line for that observer. The program finally prints value:6. Both mutation and notification behavior are visible: lack of a second callback does not mean the second domain operation was skipped. The same instance continues to hold the updated value after its subscriber is removed.

Compile the genuine external feature

The external defining file declares a public partial constructor and a field-like partial event. The implementation file supplies the matching constructor body and the event's add and remove accessors, using an explicit private delegate field. Their containing partial declarations describe one sealed Meter class, so private members are shared across the compiled type. Exactly one definition and implementation are required for these members. Constructor initializers, if needed, belong on the implementing constructor declaration. Saving all provided files matters; omitting the implementation is a compilation problem rather than an optional runtime behavior.

Pitfalls and limits

Partial events do not generate your subscription policy automatically. The implementation must supply add and remove behavior, and this sample deliberately uses ordinary delegate combination and removal. It is synchronous and makes no thread-safety promise. Retaining the subscriber delegate also matters: a new lambda expression with similar text should not be treated as the guaranteed same subscription to remove. Exceptions from handlers can still propagate through Add after its value update. Those failure and concurrency policies require deliberate application design; the partial declaration syntax does not create transactional notification or subscriber lifetime management.

Practice one: isolate a contract

First leave the observer attached for both additions and predict two callback lines, for five and six, before the final value line. Then restore removal and add a second independent observer with a distinct label. Hint: track exactly which delegate instances remain subscribed before each operation. Use a fresh meter when comparing scenarios so old subscriptions cannot confound the result. Keep direct expected values for the domain total, since inspecting callback count alone could miss a wrong addition even when notification routing appears correct.

Practice two: challenge an assumption

For a second exercise, add a validating construction rule for nonnegative initial values and put its implementation in the external constructor body. Test a valid boundary and a rejected negative input. Hint: preserve matching signatures in both partial declarations; validation does not belong in a body on the defining declaration. Then introduce a deliberately throwing observer in a separate scenario and document whether callers should see its failure after state changes. Do not advertise the baseline as atomic merely because construction and event accessors are organized across several files.

Review the resulting contract

Review partial members by checking complete file inclusion, signature agreement, exactly one required implementation, and the behavior actually supplied by its body or accessors. The event lifecycle still needs explicit subscription, removal, exception, and ownership decisions. The external project proves the stable C# 14 syntax, while the companion makes those decisions inspectable with a conventional class. Partial declarations can support tooling and source generation, but this complete hand-written example needs no generator. The compiler combines the supplied parts; neither the browser companion nor a custom runtime is secretly filling in missing members.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

C# 14 partial constructors and events lab

Set up the SDK project

  1. Save every listed file at its exact relative path in a fresh directory.
  2. Install a .NET 10 SDK supporting stable C# 14; check dotnet --version.
  3. Run dotnet run --project FeatureLab.csproj. Keep this external code separate from the .NET 9 playground.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
var meter = new Meter(2);
Action<int> observer = value => Console.WriteLine($"changed:{value}");
meter.Changed += observer;
meter.Add(3);
meter.Changed -= observer;
meter.Add(1);
Console.WriteLine($"value:{meter.Value}");
Meter.Declarations.csDownload file ↗
public sealed partial class Meter
{
    public partial Meter(int initial);
    public partial event Action<int>? Changed;
    public int Value { get; private set; }
    public void Add(int delta) { Value = checked(Value + delta); handlers?.Invoke(Value); }
}
Meter.Implementation.csDownload file ↗
public sealed partial class Meter
{
    private Action<int>? handlers;
    public partial Meter(int initial) { Value = initial; }
    public partial event Action<int>? Changed
    {
        add => handlers += value;
        remove => handlers -= value;
    }
}
FeatureLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Expected result

Expected console output, in this order: changed:5 value:6 The portable companion compares the same small rule using older syntax; it does not compile or emulate the new feature.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
var meter = new Meter(2);
Action<int> observer = value => Console.WriteLine($"changed:{value}");
meter.Changed += observer;
meter.Add(3);
meter.Changed -= observer;
meter.Add(1);
Console.WriteLine($"value:{meter.Value}");
sealed class Meter
{
    private Action<int>? handlers;
    public Meter(int initial) { Value = initial; }
    public int Value { get; private set; }
    public event Action<int>? Changed { add => handlers += value; remove => handlers -= value; }
    public void Add(int delta) { Value = checked(Value + delta); handlers?.Invoke(Value); }
}
Open C# practice

C# practice output

changed:5
value:6
Try this
Keep partial signatures matched. Removing the subscriber suppresses its second message without stopping the value change.

Further reading