AlgoMaster Logo

Partial Classes & Methods

Last Updated: May 17, 2026

12 min read

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.

What a Partial Class Is

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 Rules for Splitting

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:

RuleDescription
Same nameEvery part declares the same type name
Same namespaceEvery part lives in the same namespace (or no namespace)
partial on every partDrop it from one file and you get CS0260
Same kindAll parts must be class, all struct, all interface, or all record
Same access modifierAll parts must agree on public, internal, etc.
Same generic parametersIf 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 assemblyAll 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.

How the Compiler Merges Parts

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.

Why Partial Classes Exist

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:

  • Source generators (C# 9+). A source generator runs at compile time and emits C# code as part of the build. If the generator emits a partial class declaration, the user can write the other half in a regular file. The two halves are merged by the compiler.
  • Entity Framework Core scaffolding. 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.
  • WPF, Xamarin, and MAUI XAML-backed views. The XAML file compiles to a generated partial class containing the layout setup. The code-behind file (.xaml.cs) is the other partial. The view is one class assembled from both.
  • Protobuf and gRPC generators. Tools like 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.
  • `[GeneratedRegex]` (.NET 7+). The regex source generator emits a partial method implementation at build time. You declare the partial method, the generator implements it.

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.

Partial Methods

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:

RestrictionReason
Must return voidThe compiler may erase the call, so a return value can't be relied on
No out parametersSame reason: no body means no value to assign
Implicitly privateNo access modifier allowed
Implementation is optionalThe 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.

Partial Methods, Expanded (C# 9+)

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:

FeatureClassic (C# 3-8)Expanded (C# 9+)
Return typevoid onlyAny type
out parametersNot allowedAllowed
Access modifierNone (implicitly private)Any (public, internal, private, ...)
ImplementationOptionalRequired
Use with attributesLimitedCommon (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.

A Realistic Source Generator Scenario

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.

When to Use Partial Classes Yourself

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 class is too big and should be broken into smaller classes. Splitting it across files doesn't reduce its complexity; it just hides it.
  • The class has unrelated concerns and should follow the single-responsibility principle.
  • Helper methods should be extension methods in a separate static class.

The legitimate hand-written reasons are narrow:

  1. You own a tool-generated file that you cannot or should not edit (because regeneration would erase your changes), and you need to attach methods or interfaces to that class. Declaring your additions as partial in a separate file keeps your work safe.
  2. You're writing a class that will be consumed by a generator later, and you want to declare the user-facing half now while the generated half is being figured out.
  3. Test-only members that you want to keep in a *.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.

Limitations and Common Pitfalls

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.

A Worked Example: Split Order

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".

Summary

  • A partial class lets you split one type across multiple files. All parts must use the partial keyword and agree on namespace, name, kind, access modifier, and generic parameters.
  • The compiler merges parts at build time into a single type in the IL. There's no runtime overhead, no extra metadata, and no way to tell which file a member came from at runtime.
  • All parts must compile into the same assembly. Partials cannot span DLLs.
  • The classic form of partial methods (C# 3-8) requires void return, no out parameters, and no access modifier. Implementations are optional; missing ones get erased by the compiler along with their call sites.
  • The expanded form (C# 9+) allows any return type, out parameters, and explicit access modifiers, but the implementation is required. This form exists mainly to support source generators.
  • Partials exist for tool-generated code: Windows Forms designer, WPF/MAUI XAML, EF Core scaffolding, gRPC, protobuf, JSON source generators, [GeneratedRegex]. The pattern is "one half owned by a tool, one half by you".
  • In hand-written code, partials are rare. If you find yourself splitting a class for "organization", the class probably needs to be broken into smaller types instead.