📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are partial classes and partial members in C#? Explain how the parts are combined, why source generators depend on them, partial methods (classic and extended), partial properties (C# 13) and partial constructors and events (C# 14).

Partial types and members in C#: compile-time merging rules, generated code, partial methods, partial properties and C# 14 partial constructors and events.

Mid-Level Theory Intermediate csharp csharp-14 oop partial-class partial-members source-generators

🎯 Key points

  • Partial declarations are merged into one type at compile time; the run time sees a single type
  • All parts need partial and must be in the same assembly and namespace; modifiers, interfaces and attributes are combined
  • The main purpose is keeping generated code separate from hand-written code
  • Source generators such as `[GeneratedRegex]`, `[LoggerMessage]` and JsonSerializerContext need partial members
  • Classic partial methods are optional and their calls are removed if not implemented
  • Extended partial methods (C# 9) allow modifiers and return types but must be implemented
  • C# 13 adds partial properties and indexers; C# 14 adds partial constructors and events
  • Partial is not a substitute for splitting responsibilities into separate classes

⏱ 30-second answer

A partial type is one class, struct, record or interface split across several declarations (usually files) that the compiler merges into a single type at compile time. All parts need partial, must be in the same assembly and namespace, and their modifiers, base types and interfaces are combined. Their main purpose is separating generated code from hand-written code: designers and source generators such as [GeneratedRegex], [LoggerMessage] and JsonSerializerContext write the other half. Partial members let one part declare a member and another implement it: partial methods (classic ones may be left unimplemented and removed), partial properties and indexers (C# 13), and partial constructors and events (C# 14).

🗣 Full answer (senior)

How I would answer this in an interview

Partial types let one class, struct, interface or record be declared across multiple files; the compiler merges them into a single type. Partial members let one part declare a member and another part implement it. They exist mainly so generated code and hand-written code can live side by side without editing each other — which is why source generators depend on them.

1. How the parts are combined

// Order.cs
public sealed partial class Order
{
    public OrderId Id { get; }
    public void Submit() { Validate(); /* ... */ }
}

// Order.Validation.cs
public sealed partial class Order
{
    private void Validate() { /* ... */ }
}
  • All parts must use partial, be in the same assembly and namespace, and have the same kind (class/struct/record/interface).
  • Modifiers, base class and interfaces are merged: if any part says sealed, abstract or declares a base class, it applies to the whole type; interface lists are unioned; conflicting base classes are an error.
  • Attributes from all parts are combined.
  • At compile time there's only one type — no run-time cost.

2. Why source generators depend on them

A Roslyn source generator can only add new source files to a compilation; it can't modify yours. Partial types let it add members to your type:

public static partial class Log
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} submitted")]
    public static partial void OrderSubmitted(ILogger logger, Guid orderId);   // generator implements this
}

[JsonSerializable(typeof(OrderDto))]
internal partial class AppJsonContext : JsonSerializerContext { }          // generator adds metadata members

public static partial class Patterns
{
    [GeneratedRegex(@"^\d{11}$")]
    public static partial Regex Abn();                                     // generator emits the regex code
}

This enables compile-time code generation for logging, JSON serialisation (needed for NativeAOT/trimming), regex, P/Invoke ([LibraryImport]), configuration binding, MVVM toolkits, Mapperly mappers and more. Older uses: WinForms/WPF designer files (Form1.Designer.cs) and EF Core scaffolding.

3. Partial methods

Classic partial methods (C# 3):

partial void OnNameChanged();          // declaration, no body
  • Implicitly private, must return void, no out parameters.
  • If no part implements them, the compiler removes the declaration and all calls — zero cost. Designed for generated code offering optional hooks.

Extended partial methods (C# 9):

public static partial Regex Abn();     // must be implemented by another part
  • Can have access modifiers, non-void returns, and out parameters.
  • Must have exactly one implementation — that's what generators provide.

4. Partial properties and indexers (C# 13)

public partial class SettingsViewModel
{
    [ObservableProperty]
    public partial string DisplayName { get; set; }   // declaration; generator implements with change notification
}

One part declares the property (signature only); another provides accessor bodies. This lets generators (e.g. MVVM toolkits) implement property logic without requiring fields-with-attributes tricks. [GeneratedRegex] also works on partial properties.

5. Partial constructors and events (C# 14)

C# 14 extends the same model to instance constructors and events: a declaring part provides the signature and a single implementing part provides the body (for events, the add/remove accessors). This lets generators implement constructor logic (e.g. dependency wiring, interop initialisation) or weak-event patterns while the user declares the shape.

6. Good and bad uses by hand

  • Good: separating generated from hand-written code; very occasionally splitting a large type by concern (e.g. a big API client with nested parts) when it genuinely can't be split into separate types.
  • Bad: using partial classes to hide that a class is too big. A 3,000-line class split into five files is still a 3,000-line class with too many responsibilities.

7. A production example

We migrated an API to NativeAOT for faster start-up in Azure Container Apps. Reflection-based System.Text.Json serialisation and some Regex usages produced trimming warnings. Adding a partial JsonSerializerContext with [JsonSerializable] attributes and converting hot regexes to [GeneratedRegex] partial methods removed the warnings, cut cold start noticeably, and made serialisation faster even in the JIT build. Partial members were the mechanism that let generated code slot into our types.

🔍 What the interviewer is checking

Checks that the candidate understands the compile-time nature of partial types and the modern generator use cases.

Green flags: merging rules; same assembly; generators like [GeneratedRegex], [LoggerMessage], JSON contexts; classic versus extended partial methods; partial properties (C# 13) and constructors/events (C# 14); "not a refactoring tool".

Red flags: thinks partial exists at run time or across assemblies; uses partial to split god classes; unaware of source generators.

📚 Learn in depth

1. Compiler merging rules

  • All parts' members are combined into one symbol; duplicates (same signature) are errors unless they're a partial member declaration + implementation pair.
  • partial must be the last modifier before class/struct/record/interface (or before the return type for members).
  • Accessibility: if parts specify accessibility, they must agree.
  • Type parameters and constraints: must match across parts.
  • XML documentation: comments from multiple parts are merged (or taken from the declaring part for members).
  • Nested types can be partial too, and the containing type must be partial for a generator to add nested types into it.

2. How a source generator adds code

Incremental generators (IIncrementalGenerator) inspect the compilation via syntax and semantic models (e.g. ForAttributeWithMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute", ...)), then call context.AddSource("Patterns.g.cs", source) to emit a file like:

partial class Patterns
{
    public static partial Regex Abn() => AbnRegex.Instance;   // implementing part
    // ... generated regex runner ...
}

The generated file is part of the compilation (visible in the IDE under Dependencies → Analyzers, or emitted to disk with <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>). Because the generator can only add code, your declaration must be partial and accessible in the right way.

3. Classic partial method removal

partial class Customer
{
    partial void OnNameChanging(string value);
    public string Name { get => _name; set { OnNameChanging(value); _name = value; } }
}

If no implementation exists, the call OnNameChanging(value) is removed including argument evaluation. So OnNameChanging(ExpensiveCompute()) doesn't even call ExpensiveCompute() when unimplemented — a subtle semantic difference worth knowing.

4. Partial member rules (extended forms)

Rule Classic partial method Extended partial method/property/constructor/event
Implementation required No Yes, exactly one
Accessibility Implicitly private Explicit (public, etc.)
Return type void only Any
out parameters No Yes
Can be virtual/override/sealed No Yes

Signatures, including nullability and scoped/params modifiers, must match between declaration and implementation (with some flexibility for parameter names and attributes).

5. Generators that use partials in modern .NET

Feature Partial declaration
[LoggerMessage] partial logging methods
[GeneratedRegex] partial method or property returning Regex
[JsonSerializable] partial JsonSerializerContext
[LibraryImport] partial P/Invoke methods
Configuration binding generator intercepts calls (interceptors), less about partials
[ObservableProperty] (CommunityToolkit.Mvvm) partial properties (C# 13)
Mapperly partial mapper methods

6. Partial types and code organisation

Some teams split a type into Order.cs, Order.Validation.cs, Order.Events.cs. It can help navigation for large aggregates, but it also hides size and coupling. A better default: extract cohesive concepts into their own types (value objects, policies, domain services). Use partial splits by hand mainly for generated code and platform conventions.

Try it yourself

  1. Add a [GeneratedRegex] partial method and set EmitCompilerGeneratedFiles to inspect the output.
  2. Write a classic partial method with no implementation and an argument with a side effect; observe that it doesn't run.
  3. Create a JsonSerializerContext and compare serialisation performance and trimming warnings.
  4. Try declaring a partial type in two different namespaces and read the error.

💻 Code

// Source-generated regex (compile-time, AOT-friendly)
public static partial class Validators
{
    [GeneratedRegex(@"^[A-Z]{3}$", RegexOptions.CultureInvariant)]
    private static partial Regex CurrencyCodeRegex();

    public static bool IsCurrencyCode(string s) => CurrencyCodeRegex().IsMatch(s);
}

// Source-generated, allocation-free logging
public static partial class Log
{
    [LoggerMessage(EventId = 1001, Level = LogLevel.Information,
                   Message = "Order {OrderId} placed for {Total}")]
    public static partial void OrderPlaced(ILogger logger, int orderId, decimal total);
}

// System.Text.Json source generation for Native AOT
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(List<OrderDto>))]
internal partial class AppJsonContext : JsonSerializerContext;

// Classic optional hook: removed if never implemented
public partial class Invoice
{
    public void Save() { OnSaving(); /* persist */ }
    partial void OnSaving();                 // no implementation -> call vanishes
}

// C# 13 partial property and C# 14 partial constructor (one part each)
public partial class Customer
{
    public partial string Name { get; set; }  // declaring part
    public partial Customer(string name);     // declaring part (C# 14)
}
public partial class Customer                  // implementing part (often generated)
{
    private string _name = "";
    public partial string Name { get => _name; set => _name = value.Trim(); }
    public partial Customer(string name) => Name = name;
}

⚠ Common mistakes

1. Using partial classes to hide God classes

What happens: a huge class is split across files to look manageable. Why it is wrong: responsibilities and coupling remain; the class is still too big. Do instead: extract cohesive types; use partial splits mainly for generated code.

2. Forgetting partial on the containing type

What happens: a generator can't add members, or nested partial types fail. Why it is wrong: generated code can't merge into a non-partial type. Do instead: mark the type (and containing types) partial as the generator requires.

3. Editing generated files

What happens: developers modify .g.cs or designer files. Why it is wrong: changes are overwritten on the next build or regeneration. Do instead: put customisations in your own partial part or generator configuration.

4. Conflicting modifiers across parts

What happens: one part says public, another internal, or different base classes. Why it is wrong: compile errors or surprising merged modifiers (sealed from one part applies to all). Do instead: keep type-level modifiers in one primary part.

5. Relying on side effects in classic partial method arguments

What happens: OnChanged(Compute()) where Compute has side effects. Why it is wrong: if unimplemented, the whole call including Compute() is removed. Do instead: avoid side effects in partial method arguments.

6. Expecting partial classes across assemblies

What happens: trying to extend a type from another project with partial. Why it is wrong: all parts must be in the same assembly. Do instead: use extension methods/members, composition or inheritance.

7. Ignoring source-generated alternatives in AOT apps

What happens: reflection-based serialisation and regexes kept in trimmed/AOT apps. Why it is wrong: trimming warnings, runtime failures and slower start-up. Do instead: use partial-based generators (JsonSerializerContext, GeneratedRegex, LoggerMessage).

8. Splitting a type across many files without convention

What happens: parts scattered in different folders. Why it is wrong: developers can't find the full type definition. Do instead: keep parts together with clear naming (Order.Validation.cs).

🏛 Senior insight: trade-offs

Seniors see partial as plumbing for code generation, not as a way to organise their own code.

Need Use partial?
Source generator must add members Yes, it is required
Designer-generated UI code Yes
Organise a large class No, refactor
Optional hook in generated code Classic partial method
Generator-implemented property or constructor C# 13 partial property / C# 14 partial constructor

They connect partial to Native AOT and trimming: generators replace run-time reflection (regex, JSON, logging, P/Invoke marshalling) with compile-time code, which improves start-up and makes AOT possible.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate tries to extend another assembly's type with partial Why: Checks extension methods and C# 14 extension members
    How to answer

    A static class cannot be instantiated or inherited and contains only static members; in IL it is abstract sealed. It is the right home for stateless helpers (Math, Path), constants and extension methods, which must be declared in a non-generic, non-nested static class. C# 14 adds extension blocks (extension(Order o) { ... }) inside static classes, so you can add extension properties and static extension members too. Static classes cannot implement interfaces or be injected, so static state is global, shared across threads and hard to replace in tests. Prefer a DI-registered singleton when something has state, configuration or I/O.

  2. When: Candidate mentions C# 14 partial constructors Why: Checks constructor rules and primary constructors
    How to answer

    A constructor runs when an object is created and should leave it valid. If you declare none, the compiler adds a public parameterless one. Constructors can chain: : this(...) calls another constructor of the same class, : base(...) calls the base class's. For new Derived(), the order is: derived field initialisers, base field initialisers, base constructor body, derived constructor body. A static constructor runs once, before the type is first used, and is thread-safe. Primary constructors (C# 12) put parameters on the class declaration; they are captured as hidden fields, not properties, and are mutable. required members (C# 11) force callers to set them in an object initialiser. Calling a virtual method from a constructor runs the derived override before the derived constructor body has run, so it can see uninitialised state.

🛠 Real-world problems: how you handle them

Cutting serverless cold start by moving regexes to source generation
Situation
A document ingestion function on [Azure Functions] used about [40] regexes created with `RegexOptions.Compiled` in static fields.
Signals
Cold starts averaged [1.8 s]; traces showed about [600 ms] in regex construction. Native AOT publishing failed trimming warnings around regex use.
Investigation
Used `dotnet-trace` at start-up and confirmed regex parsing and IL emission as the hot spot.
Resolution
Moved each pattern to a `[GeneratedRegex]` partial method in a `static partial class Patterns`. The code is generated at build time, so start-up does no regex parsing.
Prevention
Enabled analyser SYSLIB1045 (use GeneratedRegex) as a warning across the solution, and added a cold-start metric to the dashboard.

How to tell it (STAR)

**Situation:** our ingestion function had slow cold starts, and profiling showed regex construction as a big part. **Task:** cut cold start without changing matching behaviour. **Action:** I moved all [40] patterns to `[GeneratedRegex]` partial methods, kept the same options, ran the existing tests and enabled SYSLIB1045 so new regexes follow the pattern. **Result:** cold start dropped by about [600 ms], and the function could later be published with Native AOT.

↩ Usually asked after

More in C# OOP Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me