TL;DR
- Expose behavior (methods) instead of public fields or collections for safer, maintainable C# code.
- Encapsulation protects invariants and enforces business rules, preventing invalid state.
- Methods provide clear, intention-revealing APIs and future-proof your design.
- Hiding internal data structures allows you to change implementations without breaking consumers.
We’ve all written classes that expose public data, a List<Order>
here, a Dictionary<string, decimal>
there. It’s tempting to just slap public
on everything and move on. But here’s what that approach costs you: control, flexibility, and peace of mind.
When you expose raw data, you’re basically handing over the keys to your object’s internals. Anyone can mess with your collections, break your business rules, or put your object in an invalid state.
Before: Exposing Data
public class OrderManager
{
public List<Order> Orders { get; set; } = new();
public decimal TotalRevenue { get; set; }
// Anyone can break our rules
}
// Consumers can do dangerous things
manager.Orders.Clear(); // Oops, lost all orders
manager.TotalRevenue = -500; // Invalid state
After: Exposing Behavior
public class OrderManager
{
private readonly List<Order> _orders = new();
private decimal _totalRevenue;
public int OrderCount => _orders.Count;
public decimal GetTotalRevenue() => _totalRevenue;
public bool HasPendingOrders() => _orders.Any(o => o.Status == OrderStatus.Pending);
public void AddOrder(Order order)
{
if (order == null) throw new ArgumentNullException(nameof(order));
_orders.Add(order);
_totalRevenue += order.Amount;
}
public IReadOnlyList<Order> GetOrdersByStatus(OrderStatus status)
=> _orders.Where(o => o.Status == status).ToList();
}
Why This Works Better
Protected invariants: Your object controls how its state changes. No more surprise bugs from external code messing with your data.
Clear contracts: Methods like HasPendingOrders()
tell consumers exactly what they can do. No guessing what properties mean or how to use them safely.
Future flexibility: Need to change how you store orders internally? Switch from a List
to a database? Your public interface stays the same.
Think of it like a coffee machine, you press buttons to get coffee, you don’t open it up and fiddle with the heating element. Same principle: expose what the object does, not how it stores things internally.
The Mental Model
Ask yourself: “What does this object help people accomplish?” Then design methods around those goals, not around your internal data structures.
Your classes become more reliable, your APIs become clearer, and your future self won’t curse your name when requirements change. Expose behavior, hide data, it’s that simple.
Frequently Asked Questions
What does it mean to expose behavior instead of data in C#?
Why is exposing public data fields or collections risky?
How does exposing behavior improve code flexibility?
What are the benefits of using methods like AddOrder or GetOrdersByStatus?
AddOrder
and GetOrdersByStatus
encapsulate business logic and validation, ensuring that only valid operations are performed. This protects the object’s invariants and provides a clear, intention-revealing API for consumers.How does exposing behavior support information hiding and encapsulation?
See other c-sharp posts
- C# Abstract Classes Explained: Practical Examples, Patterns, and Best Practices
- Abstract Class vs Interface in C#: with Real-World Examples, and When to Use Them
- C# Access Modifiers Explained: Complete Guide with Examples & Best Practices
- C# 14’s Alias Any Type: A Game-Changer for Code Readability?
- Array vs ArrayList in C#: Key Differences, Performance, and When to Use Each[+Examples]
- 5 Essential Benefits of Immutability in C# Programming
- Constructor Chaining in C#: Techniques and Best Practices
- C# Default Interface Methods vs Abstract Methods: Differences, Use Cases, and Best Practices
- Understanding Delegates vs Events in C#: When and How to Use Each
- Dictionary vs Hashtable in C#: Performance, Type Safety & When to Use Each
- C# Extension Methods: Add Functionality Without Inheritance or Wrappers
- High-Volume File Processing in C#: Efficient Patterns for Handling Thousands of Files
- Immutability vs Mutability in C#: Understanding the Differences
- Interface in C#: Contracts, Decoupling, Dependency Injection, Real-World Examples, and Best Practices
- C# Abstract Class vs Interface: 10 Real-World Questions You Should Ask
- Lambda Expressions in C#: How and When to Use Them [Practical Examples]
- Method Overloading vs Overriding in C#: Key Differences, and Examples
- C# Nullable Reference Types: How, When, and Why to Use or Disable Them
- C# 14’s params for Collections: Say Goodbye to Arrays!
- Primary Constructors in C# 12: Simplified Class Design for Classes, Structs, and Records
- Handling Cancellation in ASP.NET Core: From Browser to Database
- What Are the Risks of Exposing Public Fields or Collections in C#?
- Static Classes vs Singleton Pattern in C#: Pros, Cons, and Real-World Examples
- Task vs ValueTask in C#: Making the Right Choice for Performance
- Tuples vs Custom Types in C#: Clean Code or Lazy Hack?
- Abstract Classes in C#: When and How to Use Them Effectively [+Examples]
- C# Data Annotations: Complete Guide with Examples, Validation, and Best Practices
- C# Generics: A Complete Guide to Type-Safe, Reusable Code [With Examples]
- What is Boxing and Unboxing in C#?
- Understanding Deadlocks in C#: Causes, Examples, and Prevention
- Thread Safety in C#: Mastering Concurrency Without Race Conditions[With Examples]
- When to Use Static Classes in C#: Best Practices and Use Cases
- Why Private Fields Matter in C#: Protect Your Object’s Internal State