Last Updated: May 17, 2026
A partial class lets you split the definition of a single class across multiple files. The compiler stitches the parts back together at build time, so what looks like two or three separate files ends up as one class in the assembly. C# added this so generated code and hand-written code could live side by side without stepping on each other. This chapter covers the rules for splitting classes, partial methods (including the expanded form added in C# 9), and the kinds of problems partials are actually meant to solve.
Picture a Product class that an online store uses everywhere: the catalog page, the cart, the checkout, the admin tools. Its core shape (id, name, price, stock count) is straightforward, but it also needs a fair amount of validation logic (price must be non-negative, name must not be empty, stock must be a whole number). You could put everything in one file, and for a small class that's fine. As the validation grows, the file gets harder to navigate.
A partial class lets you put Product's data and basic methods in one file, and the validation logic in another, while still being one class to the rest of the program.
From the outside, there's one Product class with one set of members. Calling code can't tell that Describe and IsValid came from different files. The compiler reads both files, sees the partial modifier, and merges everything before generating IL.
The partial keyword is the signal. Drop it from either file and the compiler reports CS0260 ("missing partial modifier on declaration of type 'Product'; another partial declaration of this type exists"). All parts have to agree that they're partial.
The compiler is strict about how parts of a partial class line up. They have to agree on identity, otherwise the merge can't happen.
A partial class must satisfy all of these:
| Rule | Description |
|---|---|
| Same name | Every part declares the same type name |
| Same namespace | Every part lives in the same namespace (or no namespace) |
partial on every part | Drop it from one file and you get CS0260 |
| Same kind | All parts must be class, all struct, all interface, or all record |
| Same access modifier | All parts must agree on public, internal, etc. |
| Same generic parameters | If one part has <T>, every part has <T> with matching constraints |
| Same base class (if specified) | If a part declares a base class, others must either match it or omit it |
| Same assembly | All parts must compile into the same assembly. Partials cannot span DLLs. |
The interfaces a class implements are additive: each part can declare its own, and the final class implements the union. Members are also additive: each part contributes its own fields, properties, and methods, and the final class has all of them.
The final Order class implements both IComparable<Order> and IFormattable. The base class would have to match if specified, but interfaces compose freely.
What's not allowed is conflicting members. If both files declare a public int Id, the compiler reports CS0102 ("the type 'Order' already contains a definition for 'Id'"). Each member name has to be unique across the whole class, the same as if everything were in one file.
Conceptually, the compiler walks every file in the assembly, gathers each partial class Product declaration, and treats them as one type. There's no runtime stitching, no reflection trick, and no extra metadata in the output. The IL is identical to what you'd get if you'd written all the members in one file.
The diagram below shows the flow:
Three source files come in, one type goes out. The output IL has no notion of "this method came from Product.Validation.cs". The merged type is what the rest of the program sees.
Because the merge is a build-time concern, debugging works file-by-file. Stepping into IsValid lands in Product.Validation.cs, stepping into Describe lands in Product.cs. Stack traces show the right file. Source maps stay intact.
One thing to be careful about: each part still has its own using directives. A using in Product.cs doesn't apply to Product.Validation.cs. Add the imports each file needs. With global usings (.NET 6+), some namespaces are imported everywhere automatically, but anything outside that set still needs explicit using.
Without the using System.Text.Json; line in the second file, the compiler can't resolve JsonSerializer. Each file is parsed independently, so each file declares its own imports.
Partial classes were not added because programmers asked for a way to spread one class across many files for fun. They were added to solve a very specific tooling problem: how do you let a code generator write a chunk of code that the user can extend without the generator clobbering the user's edits the next time it runs?
The original use case in 2005 was Windows Forms designer files. You drag a button onto a form. Visual Studio writes the code for that button (its size, position, event wiring) into one file. You write the click handler in a second file. The next time you drag another button, Visual Studio rewrites the first file. Your second file stays untouched because it's a different file with the same partial class declaration.
That same pattern shows up in several places today:
partial class declaration, the user can write the other half in a regular file. The two halves are merged by the compiler.dotnet ef dbcontext scaffold reads a database schema and generates entity classes. The generated file is regenerated whenever the schema changes. To add custom methods to an entity, declare the entity as partial (the scaffold does this) and write your custom methods in a separate file..xaml.cs) is the other partial. The view is one class assembled from both.protoc emit C# message classes from .proto files. Generated classes are usually partial so projects can add methods (validation, helpers) without losing them when the generator re-runs.The unifying theme: one half is owned by a tool, the other half is owned by you. Splitting across files makes that ownership boundary clear. Re-running the generator doesn't touch the file you wrote.
In a hand-written codebase, partial classes are uncommon. Most teams don't reach for them without a generator in the picture, because splitting a class for "organizational" reasons usually means the class is doing too much and should be refactored, not partitioned.
A partial method is a method that's declared in one part of a partial class and optionally implemented in another. If no implementation is provided, the compiler removes the declaration and every call to it from the final IL. The call site disappears too, including any expressions used to compute the arguments.
The classic shape (C# 3 through C# 8) looked like this:
OnPriceChanging is declared once and implemented once, in a separate file. If Product.Audit.cs didn't exist, the call to OnPriceChanging would simply be erased by the compiler. The Product.cs file would still compile, and UpdatePrice would just set the price without calling anything.
The classic form had tight restrictions:
| Restriction | Reason |
|---|---|
Must return void | The compiler may erase the call, so a return value can't be relied on |
No out parameters | Same reason: no body means no value to assign |
Implicitly private | No access modifier allowed |
| Implementation is optional | The whole point of the feature |
That form is fine for hook-style callbacks like OnPriceChanging or OnNameChanged. It's the pattern Windows Forms used for designer events and that EF6 used for entity hooks.
Cost: A partial method without an implementation costs zero at runtime. The compiler removes the declaration, the call, and the argument evaluation. Even an expensive argument expression like BuildAuditPayload() is gone if no implementation is provided. Don't worry about leaving these hooks in code that no implementation file uses.
C# 9 lifted most of the old restrictions on partial methods. A partial method can now return a value, declare out parameters, and carry an access modifier. The catch: if it has any of those, an implementation is required. The compiler can no longer "erase" it because callers genuinely depend on the result.
This was added mainly to support source generators. A generator can emit a partial method declaration with [GeneratedRegex] or a similar attribute and let the build process produce the implementation. The user code calls a normal-looking method; the generated code provides the body.
HasValidSkuFormat is public and returns bool. The declaring side never says how the check works. The implementing side fills it in. The user reading Product.cs sees the API; the generator handles the gritty regex.
The expanded form rules:
| Feature | Classic (C# 3-8) | Expanded (C# 9+) |
|---|---|---|
| Return type | void only | Any type |
out parameters | Not allowed | Allowed |
| Access modifier | None (implicitly private) | Any (public, internal, private, ...) |
| Implementation | Optional | Required |
| Use with attributes | Limited | Common (source generators) |
The compiler decides which form it's looking at based on the declaration. If the declaration is private partial void OnX() (or partial void OnX(), since private is the default), classic rules apply and the implementation is optional. If it has any of out, a non-void return, or an explicit access modifier, expanded rules apply and the implementation must be supplied.
Forget the implementation in the expanded form and the compiler reports CS8795 ("partial method 'HasValidSkuFormat' must have an implementation part because it has accessibility modifiers"). The compiler refuses to invent a body.
Source generators are covered in detail in the Reflection & Attributes section. For now, the partials story is enough to see why they show up.
The System.Text.RegularExpressions library ships with [GeneratedRegex], an attribute that triggers a source generator at compile time. You declare a partial method with the attribute, and the generator emits an implementation that compiles the regex once at build time instead of at first use.
You don't see the generator output unless you go looking for it (Visual Studio's "Show generated files" or the EmitCompilerGeneratedFiles MSBuild property), but it's there in a separate .g.cs file that the build pipeline added. That file has the matching partial declaration with the actual implementation, including a compiled regex state machine.
The pattern is the same as the manual HasValidSkuFormat example a section back: the user declares the partial method's shape, something else fills in the body. The user-facing file stays clean of generated noise. Re-running the build regenerates the body if needed; the user's file never changes.
The same shape powers JSON source generators ([JsonSerializable]), gRPC clients, COM interop wrappers, and a growing list of analyzers that opt into source generation. Anywhere you see a partial method declaration with an attribute on it, there's a generator filling in the body.
In hand-written code, the answer is usually "rarely". Most reasons to reach for partial indicate that one of these would serve you better:
The legitimate hand-written reasons are narrow:
partial in a separate file keeps your work safe.*.Tests.cs companion file inside the same assembly. Some teams use partials for this; others prefer [InternalsVisibleTo] plus a separate test class. Personal taste.The rule of thumb: if no tool is generating one of the parts, you probably don't want a partial class. Use composition, smaller classes, or extension methods instead.
If the database schema changes and the scaffold is re-run, Order.cs gets rewritten but Order.Behavior.cs stays untouched. The custom methods survive. That's the whole point.
A bad use of partial would be splitting Product across Product.cs, Product.Pricing.cs, Product.Inventory.cs, and Product.Reporting.cs just because the file is long. Those concerns suggest separate classes: ProductPricing, InventoryService, ProductReport. The original Product should stay small and focused.
A few things trip people up the first time they work with partials.
Partials cannot span assemblies. Every part has to compile into the same DLL. There's no way to start Product in one project and finish it in another. If the parts are in different assemblies, the compiler treats them as two separate types with the same name in different namespaces (or fails outright). The link is purely a build-time concept.
Partials don't add multiple inheritance. A class can declare a single base class once. If two parts declare a base, they have to agree:
The compiler rejects this with CS0263. Each part can declare its own interfaces (those compose), but only one base class is allowed across the whole type.
Attributes are additive. If both parts apply attributes to the class, the final class carries the union. There's no override or "winner" rule. Same with attributes on members.
The final Order has both [Serializable] and [DebuggerDisplay(...)]. This is sometimes useful (different files contribute different attributes for different tools) and sometimes a footgun (you forget the second file is also adding [Obsolete]).
`using` directives are file-local. Each file declares the imports it needs. A using in Product.cs doesn't apply to Product.Validation.cs. With global usings (.NET 6+), some namespaces are imported across all files, but anything outside the global set has to be re-declared per file.
Partial methods have their own rules. Only methods declared inside a partial class can be partial. You can't make a top-level method partial. Same goes for properties, indexers, and events. C# 13 added partial properties, so those can now be split, but earlier versions cannot.
Reflection sees one type. If you call typeof(Product).GetMethods(), you get the union of methods from every part. Reflection has no notion of "which file this came from" because the type system has no notion of that. Stack traces and PDBs are the only places file information is preserved.
Naming. There's no required convention, but most projects use OriginalName.SuffixDescribingTheRole.cs (for example, Product.Validation.cs, Order.Generated.cs, Customer.Designer.cs). The IDE's "find file" works better with this convention, and code reviewers can tell from the filename what role each part plays.
The complete Order class below pulls all the ideas together. Three files. One core, one validation, one formatting. Each is small and focused, and the merged type behaves as a single class.
Three files, one class. Subtotal (declared in Order.cs) is called from ToReceipt (declared in Order.Formatting.cs) with no extra ceremony. Across files, the members behave as if they were all written next to each other.
If this code were in a real codebase, you'd probably write Order in one file, because three small files for one class isn't worth the navigation cost. The example shows the mechanics, not best practice. In real life, the multi-file split would be justified by something like "Order.cs is scaffolded by EF Core" or "Order.Generated.cs is emitted by a JSON source generator".
partial keyword and agree on namespace, name, kind, access modifier, and generic parameters.void return, no out parameters, and no access modifier. Implementations are optional; missing ones get erased by the compiler along with their call sites.out parameters, and explicit access modifiers, but the implementation is required. This form exists mainly to support source generators.[GeneratedRegex]. The pattern is "one half owned by a tool, one half by you".