.NETPro #21 | Special Edition: Final Part of Our Exclusive Series with Mark J. Price — Master Partial Events and Constructors in C# 14
Partial Types and Members: Breaking Apart and Reassembling C# Classes: Part 4
What a journey it has been! Over the past few months, through Parts 1, 2, and 3 of the Partial Types and Members: Breaking Apart and Reassembling C# Classes series with Mark J. Price, we have explored how C# helps you break apart and reassemble your code in smarter, more intentional ways. From partial classes that keep projects tidy, to partial methods that introduce invisible hooks, to partial properties that bring clarity to accessors, each feature has built toward something bigger.
Now, in this final part of the series, we arrive at the grand finale with C# 14. Mark guides us through the newest additions to the partial family: partial events and partial constructors. You will see how they carry the same elegant pattern into event handling and object creation, giving both developers and source generators more room to shape clean, flexible architectures.
It is the perfect way to bring our series to a close: practical, forward-looking, and full of ideas you can carry straight into your next .NET 10 project!
Before we dive deeper into the final chapter, I want to say how much it means to us that you’ve been part of this journey. Once you’ve made your way through this edition, we would truly appreciate it if you could take a minute to share your thoughts on how helpful you found this series and what you would love to see us work on next.
Here’s what’s waiting for you in this issue:
➡️ Discover how partial events in C# 14 separate event declaration from add and remove logic
➡️ Learn how partial constructors let you spread object initialization across multiple files
➡️ Explore real-world examples using weak events and code-generation patterns
➡️ Wrap up with a quick Q&A to test your knowledge and reinforce what you’ve learned
Meet the Expert!
Before you jump in, check out these highlights from our last two editions, in case you missed them:
➡️ Learn how to modernize .NET applications with GitHub Copilot Agent Mode
➡️ C# 14 delivers extension members and significant updates for modern development
➡️ .NET 10 adds native post-quantum cryptography with four new algorithms
➡️ GitHub introduces Copilot Custom Agents designed to make .NET development smarter and faster
➡️ Andrew Lock explains how .NET 10 makes reflection easier with the new [UnsafeAccessorType] attribute
Cheers!
Adrija Mitra
Editor-in-Chief
📱Partial Events & Partial Constructors (C# 14)
Want to revisit the first three parts of this series before diving in? Check them out here: [Part 1], [Part 2], and [Part 3].
Let’s kick off today’s exploration with partial events and then glide into constructors!
📖 Partial Events – Customizing Event Behavior
(Available in C# 14)
Events are another area that historically could not be partial, but that is changing. In C# 14 with .NET 10, you can declare partial events. This allows you to separate an event’s declaration (its name and delegate type) from its add/remove accessor implementations, similar to how partial properties work.
In standard C#, an event can be declared in two ways:
Auto-implemented (field-like) event: Example:
public event EventHandler SomethingHappened;.
The compiler automatically provides backing storage (a hidden delegate field) and default add/remove implementations that simply attach or detach the delegate.Custom event: Example:
private EventHandler _somethingHappened;
public event EventHandler SomethingHappened
{
add
{
/* custom code, e.g., thread-safety or logging */
_somethingHappened += value;
}
remove
{
/* custom remove logic */
_somethingHappened -= value;
}
}This allows you to control how listeners are added and removed (for example, by using a special event manager or restricting access). Partial events are designed to support scenarios where a generator or library provides those custom add/remove behaviors behind the scenes.
One notable use case is weak events. In .NET, if an event subscriber forgets to unsubscribe, it can prevent the publisher from being garbage-collected. Weak event patterns address this by avoiding strong references to handlers. However, implementing weak events manually requires a lot of boilerplate code (often involving WeakReference).
With partial events, a library could expose an attribute like [WeakEvent] that you apply to a partial event declaration. A source generator could then implement the add/remove accessors to store handlers in a weak reference list automatically.
Before we dive into the example in the next section, here’s a quick reminder: for clarity (and a bit of fun), the examples in this series will borrow characters and concepts from the TV series Severance. Think of a partial class like a Lumon Industries employee with a split identity: the “innie” (at work) and the “outie” (outside work) live in separate worlds but together form one person. Similarly, partial classes allow different pieces of a class to live in different files yet unify into one complete type when compiled, or in Severance terms, reintegrated.
Example: Partial Event for Data Completion
Suppose that in our Lumon office app, the Macrodata Refinement process raises an event when a batch of data is completed and ready for review. We want to use a custom event mechanism, perhaps to log every subscription or to handle it in a special way. In that case, we can declare a partial event and implement it manually, as shown in the following code:
// File: MacroDataRefinement.Part1.cs
public partial class MacroDataRefinement
{
// Defining declaration of a partial event
public partial event Action<int> DataBatchCompleted;
public void CompleteBatch(int itemsProcessed)
{
Console.WriteLine($”Batch of {itemsProcessed} items processed.”);
// Raise the event (if implemented)
OnDataBatchCompleted(itemsProcessed);
}
// A helper to raise the event (not strictly required, but common pattern)
private void OnDataBatchCompleted(int count)
{
// In a real scenario, this might be generated or in the implementing part
// We’ll call the event invocation logic (to be provided in the other part)
_onDataBatchCompletedHandlers?.Invoke(count);
}
}
// File: MacroDataRefinement.Part2.cs
public partial class MacroDataRefinement
{
// A backing field for handlers (could use a WeakEvent pattern here)
private Action<int>? _onDataBatchCompletedHandlers;
// Implementing declaration of the partial event
public partial event Action<int> DataBatchCompleted
{
add
{
Console.WriteLine(”[Log] Handler subscribed to DataBatchCompleted.”);
_onDataBatchCompletedHandlers += value;
}
remove
{
Console.WriteLine(”[Log] Handler unsubscribed from DataBatchCompleted.”);
_onDataBatchCompletedHandlers -= value;
}
}
}In the preceding code, note the following:
In MacroDataRefinement.Part1.cs, we declare a partial event DataBatchCompleted of type Action<int> (meaning event handlers are methods that take an int parameter). There is no add/remove implementation given here, so this is essentially saying, “there will be an event by this name, trust me.”
We also have a CompleteBatch(int) method that does some work and then calls an OnDataBatchCompleted(count) helper.
In Part2.cs, we provide the actual event backing field and the custom add/remove accessors. Whenever someone subscribes (+=) to DataBatchCompleted, the add accessor logs a message and adds the handler to a delegate field. The same applies for unsubscribe.
The _onDataBatchCompletedHandlers delegate is then invoked in OnDataBatchCompleted to notify listeners.
Using this partial event might look as shown in the following code:
// Usage example (not part of class):
MacroDataRefinement mdr = new();
mdr.DataBatchCompleted += count =>
Console.WriteLine($”*** Notified: completed {count} items.”);
mdr.CompleteBatch(5);
mdr.DataBatchCompleted -= null; // unsubscribing would log as well
// Expected output:
// [Log] Handler subscribed to DataBatchCompleted.
// Batch of 5 items processed.
// *** Notified: completed 5 items.
// [Log] Handler unsubscribed from DataBatchCompleted.This example demonstrates a custom event implementation through partial definitions. In real life, you might not manually write the logging; instead, a generator could create the add/remove logic based on an attribute.
For instance, if we had used [WeakEvent] partial event Action<int> DataBatchCompleted; in the first part, the generator could produce an implementation that stores handlers in a WeakEventManager.
Caveats and requirements
Partial events require that exactly one part provides the implementation and one part provides the definition. You cannot have two different implementations or two definitions for the same partial event, as described here: What’s new in C# 14 | More partial members.
In practice, you will declare the event (with no add/remove) in one file and implement add/remove in another. The implementing part must include both an add and a remove block, because you need both to handle subscriptions. The defining part, on the other hand, looks just like a field-like event declaration with no body. If you declare a partial event and fail to implement it anywhere, the compiler will produce an error. Unlike partial methods, there is no “just remove it” option for events.
Also, because of how events work, if the defining declaration uses the event in other code (such as invoking it), the compiler will likely require that to be done through a helper or in the implementing part. In our example, we used a private delegate field and method to raise the event.
📝 Spotted something worth calling out? We’d love to hear about it—just drop us a quick note through this 1-min survey. We’re all ears!
📖 Partial Instance Constructors – Building Objects in Pieces
(Available in C# 14)
Another new addition to the partial capabilities is partial constructors. A partial constructor allows you to split the definition of a class’s constructor between different partial class files. One partial file declares the constructor signature (and parameters), and another provides the actual body. This feature is available with C# 14 and .NET 10 alongside partial events.
Why would you want a partial constructor?
One major use case is in interop and platform invoke scenarios. For example, Xamarin (now part of .NET for iOS and Android) can generate C# bindings for native APIs. They might want to expose a C# constructor that actually calls into a native library. With partial constructors, the binding generator can declare a constructor with the appropriate signature and perhaps attributes like [DllImport], and then provide the invocation code in another file. Similarly, a source generator might generate a constructor for a record or entity and let you add extra logic.
Another use is combining generated code with manual code during object initialization. Suppose a tool generates a class with several properties and wants to ensure that certain setup happens when the object is created, while still allowing you to inject additional logic. A partial constructor can act as a hook, similar to partial methods but used for object construction.
Example: Conditional setup in Employee constructor
Let’s return to our Employee example. Perhaps an HR tool generates a basic Employee class with a constructor, but we want to add some custom side effects when an employee is instantiated, such as logging whether they are part of the severance program. We can simulate that with a partial constructor:
// File: Employee.Generated.cs (generated definition)
public partial class Employee
{
public string Name { get; }
public bool IsSevered { get; }
// Partial constructor declaration (no body here)
public partial Employee(string name, bool isSevered);
}
// File: Employee.Custom.cs (our implementation)
public partial class Employee
{
// Implementation of the partial constructor
public partial Employee(string name, bool isSevered)
{
Name = name;
IsSevered = isSevered;
if (isSevered)
{
Console.WriteLine($”{Name} has undergone the severance procedure.”);
}
else
{
Console.WriteLine($”{Name} is not severed (outside knowledge intact).”);
}
}
}
// Usage
Employee mark = new(”Mark”, true);
Employee devon = new(”Devon”, false);
// Expected output:
// Mark has undergone the severance procedure.
// Devon is not severed (outside knowledge intact).In Employee.Generated.cs, we define the class properties and declare a public partial Employee(string name, bool isSevered); constructor, but with no body. This could represent code generated from a template, perhaps based on a database schema or an API.
In Employee.Custom.cs, we write the body of that constructor: we assign the fields and add some extra logging. When new Employee(”Mark”, true) is called, it runs the code in our partial constructor implementation. The benefit is that the generated file does not need to know anything about our extra logging logic, and if the generator regenerates the class (for example, if we add a new field), our custom code remains separate.
Some points about partial constructors per the proposal:
Only instance constructors (including primary constructors of record/class) can be partial. Static constructors wouldn’t make much sense to split (and there can be only one static ctor anyway).
There must be exactly one defining declaration and one implementing declaration for the constructor; you can’t have two partial implementations of the same ctor.
The implementing part is the only one that can have a constructor initializer (i.e., call : base(...) or : this(...)). The defining part just provides the parameter list.
You can have multiple constructors in a class and make some of them partial and some normal, or multiple partial ones (each would need its own implementation).
If a partial constructor is declared but not implemented anywhere, that’s a compile error (unlike old-school partial methods, you can’t leave a constructor without implementation). After all, what would it mean to call a constructor that doesn’t exist? So the expectation is that a generator and a developer (or two generators) coordinate to always supply the implementation for any declared partial ctor.
It is an exciting addition because it rounds out the language: methods, properties, events, and constructors can all be partial in C# 14, providing a flexible toolkit for code generation and large-scale project organization. However, some type members still cannot be partial, including static constructors, finalizers, operators, delegates, and enums.
More details can be found in the official documentation: Partial type – C# reference | Microsoft Learn.
📖 Bringing it all together!
Partial classes and partial members are powerful features for organizing and generating code in C#. They allow a separation of concerns not only at the design level but also at the source-file level. Different aspects of a class can live in different files and be stitched together by the compiler. We saw how partial classes (introduced in 2005) enable splitting a class much like Severance splits personalities, which is especially useful for code generators and team collaboration.
Partial methods came next, offering hooks that can be implemented or ignored with zero cost if not needed. Fast forward to the mid-2020s: partial properties and indexers were added to support more advanced source generator scenarios, such as injecting code into property accessors. With C# 14, partial members now extend to events and constructors, completing the picture.
To summarize, the partial keyword can decorate the following members:
Methods
Properties
Indexers
Events
Instance constructors
But in C# 14 and earlier, the partial keyword cannot decorate:
Static constructors
Finalizers
Overloaded operators
Delegates
Enums
It is worth noting that while partial features are extremely useful in certain scenarios, you should use them only when they make sense. In many cases, regular classes and members are perfectly adequate and simpler to understand. Partial classes shine when there is generated code involved or when organizing a very large class. Partial methods and similar features are usually introduced by code generation, and you may encounter them in auto-generated files or advanced frameworks. If you do use them manually, make sure it is for clarity or a specific need, such as splitting a class across files for organizational purposes or providing an optional hook that can be omitted without side effects.
By understanding partial classes and members, you can better navigate large C# projects, especially those shaped by designers or code generators, and create cleaner APIs yourself. Just as the employees in Severance eventually discover, sometimes a clear separation can be extremely useful, as long as you remember that everything ultimately comes together to form a single whole!
The official documentation provides more examples and rules for using the partial keyword in types and members.
➡️ Microsoft Learn – Partial Classes and Methods
➡️ Microsoft Learn - Partial type (C# Reference)
📝 Spotted something worth calling out? We’d love to hear about it—just drop us a quick note through this 1-min survey. We’re all ears!
⏱️ Quick Q&A Corner
Quick bites to level up your understanding
Q1: What are partial events in C# 14, and how do they differ from regular events?
A: Partial events let you separate an event’s declaration from its add and remove logic across partial class files. You define the event’s signature in one file and implement its accessors in another. This allows injecting custom behavior, such as logging or thread safety, into event subscriptions without altering the event’s public API or cluttering the main logic.
Q2: How do partial events facilitate implementing weak events to prevent memory leaks?
A: In .NET, events hold strong references to subscribers, which can lead to memory leaks if handlers aren’t unsubscribed. Partial events allow libraries to inject weak-reference handling into the event logic. For example, a generator could apply [WeakEvent] to a partial event and implement the accessors using WeakReference, ensuring subscribers don’t prevent the publisher from being garbage-collected.
Q3: What are partial instance constructors in C# 14 and when would you use them?
A: A partial instance constructor allows a class’s constructor to be split between separate partial class files. One file declares the constructor signature (parameters), and another implements its body. This is useful for interop and code generation scenarios. For example, Xamarin’s binding generator might declare a partial constructor (with attributes for a native call) and generate the implementation that invokes the native library.
Q4: What are some rules or caveats to keep in mind with partial constructors?
A: You must have exactly one partial constructor definition (just a signature with no body) and one implementation with the same parameter list. Declaring a partial constructor without providing its implementation causes a compile-time error. Additionally, only the implementing part can include a constructor initializer (like : base(...) or : this(...)), and partial constructors can only be used for instance constructors (not static ones).
And that’s a wrap 🎬
As we wrap up this four-part journey, I truly hope you enjoyed exploring the world of partial types and members as much as we enjoyed creating this series for you. It has been a joy walking through these ideas together, and we hope you found something here that will make your everyday C# work a little cleaner, a little easier, and maybe even a little more fun.
Thank you for joining us on this special adventure with Mark J. Price. Your curiosity and enthusiasm are what inspire us to keep building guides and series that genuinely support your growth as a developer. If you have a moment, we would love to hear your thoughts on the series — what worked well for you, what could be better, and what you’d like to see in the future editions of .NETPro. Your feedback helps us shape what comes next.
Thanks for following along! Until next time, keep learning and keep building.



