Why Partial Types Matter
Partial classes and methods give you flexibility when organizing code across multiple files. You'll find them particularly useful when working with generated code, large classes that need better organization, or when you want to separate different concerns within a single type.
Modern C# development relies heavily on code generation through tools like Entity Framework, WPF designers, and source generators. Partial types let these tools generate code in separate files without touching your hand-written code. This keeps your codebase maintainable and prevents merge conflicts.
You'll learn how to split classes across files, use partial methods for extensibility points, work with source generators, and follow best practices that keep your code clean and organized.
Getting Started with Partial Classes
A partial class lets you split the definition of a class across multiple files. The compiler merges all parts together when building your project. All parts must use the partial keyword, be in the same namespace, and have the same accessibility level.
namespace MyApp.Models;
public partial class Customer
{
// Core properties
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
// Business logic
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
public bool IsEmailValid()
{
return Email?.Contains("@") == true;
}
}
namespace MyApp.Models;
public partial class Customer
{
// Validation methods in a separate file
public bool ValidateForCreation()
{
return !string.IsNullOrEmpty(FirstName) &&
!string.IsNullOrEmpty(LastName) &&
IsEmailValid();
}
public List<string> GetValidationErrors()
{
var errors = new List<string>();
if (string.IsNullOrEmpty(FirstName))
errors.Add("First name is required");
if (string.IsNullOrEmpty(LastName))
errors.Add("Last name is required");
if (!IsEmailValid())
errors.Add("Valid email is required");
return errors;
}
}
These two files define different aspects of the same Customer class. When you compile your project, C# treats them as a single class. This approach helps you organize related functionality without creating a massive single file.
Working with Generated Code
The most common use for partial classes is separating your code from generated code. Tools like Entity Framework and WPF designers create partial classes automatically. You extend these classes by creating your own partial definition.
// This file is auto-generated. Do not modify.
namespace MyApp.Data;
public partial class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public DateTime CreatedDate { get; set; }
// Navigation properties
public virtual Category Category { get; set; }
public virtual ICollection<OrderItem> OrderItems { get; set; }
}
namespace MyApp.Data;
public partial class Product
{
// Add business logic without touching generated code
public decimal GetDiscountedPrice(decimal discountPercent)
{
return Price * (1 - discountPercent / 100);
}
public bool IsNew()
{
return CreatedDate > DateTime.Now.AddDays(-30);
}
public string GetDisplayName()
{
return IsNew() ? $"NEW: {Name}" : Name;
}
// Add validation
public bool IsValid()
{
return !string.IsNullOrEmpty(Name) &&
Price > 0 &&
CategoryId > 0;
}
}
When the tool regenerates Product.Generated.cs, your custom code in Product.cs stays untouched. This separation prevents your logic from being overwritten and makes it clear which code is yours and which is generated.
Understanding Partial Methods
Partial methods let you declare a method signature in one part of a partial class and optionally implement it in another part. If you don't provide an implementation, the compiler removes the method and all calls to it. This is perfect for extensibility hooks in generated code.
namespace MyApp.Models;
public partial class Entity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// Partial method declarations (extensibility hooks)
partial void OnCreating();
partial void OnCreated();
partial void OnUpdating();
partial void OnUpdated();
public void Create()
{
OnCreating();
CreatedAt = DateTime.UtcNow;
// Save to database
OnCreated();
}
public void Update()
{
OnUpdating();
UpdatedAt = DateTime.UtcNow;
// Save to database
OnUpdated();
}
}
namespace MyApp.Models;
public partial class Entity
{
// Implement only the hooks you need
partial void OnCreating()
{
Console.WriteLine($"Creating entity with ID: {Id}");
ValidateBeforeCreate();
}
partial void OnCreated()
{
Console.WriteLine($"Entity created successfully: {Id}");
NotifyCreated();
}
// OnUpdating and OnUpdated are not implemented
// The compiler removes them and their calls
private void ValidateBeforeCreate()
{
if (Id <= 0)
throw new InvalidOperationException("Invalid ID");
}
private void NotifyCreated()
{
// Send notification
}
}
Because you only implemented OnCreating and OnCreated, the compiler removes OnUpdating and OnUpdated entirely. This means zero runtime overhead for hooks you don't use.
Modern Partial Methods with Return Values
Since C# 9, partial methods can have return types, out parameters, and accessibility modifiers if they have an implementation. This makes them more powerful while still supporting the optional implementation pattern.
namespace MyApp.Services;
// File 1: Declaration
public partial class DataProcessor
{
// Modern partial method with return value and accessibility
public partial bool ValidateData(string data, out string errorMessage);
// Traditional partial method (optional implementation)
partial void OnDataProcessed();
public ProcessResult Process(string data)
{
// This must be implemented since it has a return value
if (!ValidateData(data, out string error))
{
return ProcessResult.Failed(error);
}
// Process the data
var result = DoProcessing(data);
// This is optional
OnDataProcessed();
return result;
}
private ProcessResult DoProcessing(string data)
{
return ProcessResult.Success(data);
}
}
// File 2: Implementation
public partial class DataProcessor
{
// Must provide implementation since it has return type
public partial bool ValidateData(string data, out string errorMessage)
{
if (string.IsNullOrEmpty(data))
{
errorMessage = "Data cannot be empty";
return false;
}
if (data.Length > 1000)
{
errorMessage = "Data exceeds maximum length";
return false;
}
errorMessage = null;
return true;
}
// OnDataProcessed is not implemented - compiler removes it
}
public class ProcessResult
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
public static ProcessResult Success(string data) =>
new() { IsSuccess = true, Message = "Processed successfully" };
public static ProcessResult Failed(string error) =>
new() { IsSuccess = false, Message = error };
}
The key difference is that partial methods with return types, accessibility modifiers, or out parameters must have an implementation. The compiler won't remove them automatically.
Leveraging Source Generators with Partial Classes
Source generators are compile-time tools that analyze your code and generate additional files automatically. They work beautifully with partial classes because they can add functionality to your types without modifying your source files.
using System.Text.Json.Serialization;
namespace MyApp.Models;
// Mark as partial to allow source generator to extend it
[JsonSerializable(typeof(Person))]
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
// The JsonSerializable attribute triggers a source generator
// that creates serialization code at compile time
// Auto-generated code - Do not modify
namespace MyApp.Models;
public partial class Person
{
// Generated serialization methods
public string ToJson()
{
return System.Text.Json.JsonSerializer.Serialize(this);
}
public static Person FromJson(string json)
{
return System.Text.Json.JsonSerializer.Deserialize<Person>(json);
}
}
// Usage in your code:
// var person = new Person { FirstName = "John", LastName = "Doe" };
// string json = person.ToJson();
// Person restored = Person.FromJson(json);
Source generators run during compilation, so there's no reflection overhead at runtime. This makes them perfect for performance-critical scenarios where you'd traditionally use reflection-based serialization.
Partial Classes in MVVM Applications
MVVM frameworks often use partial classes to separate generated property change notification code from your business logic. This keeps your ViewModels clean and focuses your attention on what matters.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp.ViewModels;
// ObservableObject base provides INotifyPropertyChanged
public partial class CustomerViewModel : ObservableObject
{
// ObservableProperty generates the full property with change notification
[ObservableProperty]
private string firstName;
[ObservableProperty]
private string lastName;
[ObservableProperty]
private string email;
[ObservableProperty]
private bool isActive;
// RelayCommand generates the command implementation
[RelayCommand]
private void Save()
{
if (ValidateCustomer())
{
SaveToDatabase();
}
}
[RelayCommand]
private async Task LoadCustomerAsync(int customerId)
{
var customer = await _customerService.GetByIdAsync(customerId);
FirstName = customer.FirstName;
LastName = customer.LastName;
Email = customer.Email;
IsActive = customer.IsActive;
}
private bool ValidateCustomer()
{
return !string.IsNullOrEmpty(FirstName) &&
!string.IsNullOrEmpty(Email);
}
private void SaveToDatabase()
{
// Save logic
}
}
// Auto-generated by MVVM Toolkit
namespace MyApp.ViewModels;
public partial class CustomerViewModel
{
// Generated property with change notification
public string FirstName
{
get => firstName;
set => SetProperty(ref firstName, value);
}
public string LastName
{
get => lastName;
set => SetProperty(ref lastName, value);
}
public string Email
{
get => email;
set => SetProperty(ref email, value);
}
public bool IsActive
{
get => isActive;
set => SetProperty(ref isActive, value);
}
// Generated commands
private RelayCommand _saveCommand;
public RelayCommand SaveCommand =>
_saveCommand ??= new RelayCommand(Save);
private AsyncRelayCommand<int> _loadCustomerCommand;
public AsyncRelayCommand<int> LoadCustomerCommand =>
_loadCustomerCommand ??= new AsyncRelayCommand<int>(LoadCustomerAsync);
}
The source generator creates all the property change notification and command infrastructure. You write minimal code while getting full MVVM functionality. This dramatically reduces boilerplate in your ViewModels.
Organizing Large Classes Effectively
When a class grows large, splitting it across files based on responsibility makes it easier to work with. This isn't about hiding complexity but about making each file focus on one aspect of the class.
namespace MyApp.Services;
public partial class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
ValidateOrderRequest(request);
var order = MapToOrder(request);
await _orderRepository.AddAsync(order);
await SendOrderConfirmationAsync(order);
return order;
}
public async Task<Order> GetOrderByIdAsync(int orderId)
{
return await _orderRepository.GetByIdAsync(orderId);
}
}
namespace MyApp.Services;
public partial class OrderService
{
private void ValidateOrderRequest(CreateOrderRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.CustomerId <= 0)
throw new ValidationException("Invalid customer ID");
if (request.Items == null || !request.Items.Any())
throw new ValidationException("Order must contain items");
ValidateOrderItems(request.Items);
ValidateShippingAddress(request.ShippingAddress);
}
private void ValidateOrderItems(List<OrderItemRequest> items)
{
foreach (var item in items)
{
if (item.Quantity <= 0)
throw new ValidationException("Item quantity must be positive");
if (item.Price < 0)
throw new ValidationException("Item price cannot be negative");
}
}
private void ValidateShippingAddress(Address address)
{
if (address == null)
throw new ValidationException("Shipping address required");
if (string.IsNullOrEmpty(address.Street))
throw new ValidationException("Street address required");
}
}
namespace MyApp.Services;
public partial class OrderService
{
private async Task SendOrderConfirmationAsync(Order order)
{
var email = BuildConfirmationEmail(order);
await SendEmailAsync(email);
_logger.LogInformation(
"Order confirmation sent for order {OrderId}",
order.Id);
}
private EmailMessage BuildConfirmationEmail(Order order)
{
return new EmailMessage
{
To = order.CustomerEmail,
Subject = $"Order Confirmation #{order.Id}",
Body = FormatOrderDetails(order)
};
}
private string FormatOrderDetails(Order order)
{
var details = $"Order #{order.Id}\n";
details += $"Total: ${order.Total:F2}\n\n";
details += "Items:\n";
foreach (var item in order.Items)
{
details += $"- {item.ProductName} x {item.Quantity}\n";
}
return details;
}
private async Task SendEmailAsync(EmailMessage message)
{
// Email sending implementation
await Task.Delay(100);
}
}
Each file handles one responsibility: core operations, validation, or notifications. When you need to modify validation rules, you go straight to OrderService.Validation.cs. This organization scales better than a single 1000-line file.
Best Practices for Partial Types
Following these guidelines will help you use partial classes and methods effectively:
Use consistent naming: When splitting a class across files, use descriptive suffixes like Customer.Validation.cs or Customer.Helpers.cs. This makes it immediately clear what each file contains.
Keep generated code separate: Always put generated code in files with .g.cs or .Generated.cs extensions. This signals to developers that they shouldn't modify these files manually.
Don't overuse partials: Just because you can split a class doesn't mean you should. If your class is small or has a single clear responsibility, keep it in one file. Use partials when they genuinely improve organization.
Match accessibility across parts: All partial declarations must have the same access modifier. You can't have one part public and another internal. The compiler enforces this rule.
Organize by responsibility: When splitting large classes, group related methods together. Put all validation in one file, all data access in another, and business logic in a third. This makes your code easier to navigate.
Consider alternatives first: If you're splitting a class because it has too many responsibilities, you might need separate classes instead. Partial classes organize code within a type but don't change the fact that it's still one class doing multiple things.
Document your partials: Add comments explaining why you split a class. Future developers will appreciate understanding your reasoning, especially in codebases where partials are uncommon.