There's a latent problem in BiblioTech's model as it stands so far: any part of the program
can write book1.Available = true; directly, without going through Lend() or Return(),
leaving the catalog in an inconsistent state (for example, a book marked available that's
actually still in a member's hands). Encapsulation is the object-oriented programming
principle that solves exactly this: hiding an object's internal state and exposing only the
controlled operations through which that state can change. In this lesson you'll learn C#'s
access modifiers and close that back door on Available, so it can only be modified through
the methods designed for that purpose.
Contents
- What encapsulation is and why it matters
- Access modifiers:
public,private,protected,internal - The concrete problem:
Availablefreely modifiable private set: exposing reading, restricting writing- Properties with validation logic in the
set - Read-only properties
- Applying encapsulation to the rest of the model:
MemberandLoan - The principle of minimal exposure
- What encapsulation is and why it matters
Encapsulation consists of hiding the internal details of how an object stores and manages
its state, exposing to the outside only a controlled interface of valid operations. The core
idea is: code outside the class shouldn't be able to leave an object in an inconsistent
state. Until now, any code with access to a Book object can write
book1.Available = false; without ever having called Lend(), completely bypassing the logic
that decides whether the loan is valid (for example, checking that the book was actually
available before lending it). Encapsulation closes off that escape route.
- Access modifiers:
public, private, protected, internal
public, private, protected, internalC# offers several access modifiers that control from where a class member (property, method, field) can be used:
| Modifier | Accessible from... |
|---|---|
public |
Any part of the program, with no restrictions |
private |
Only from within the class itself |
protected |
From the class itself and from any class that inherits from it |
internal |
From anywhere in the same assembly (compiled project), but not from other external projects |
A short, generic example, outside BiblioTech's model, helps pin down the difference between
private and protected:
class Base
{
private int privateValue = 1;
protected int protectedValue = 2;
public int publicValue = 3;
public void ShowFromBase()
{
Console.WriteLine(privateValue); // Ok: same class
Console.WriteLine(protectedValue); // Ok: same class
}
}
class Derived : Base
{
public void ShowFromDerived()
{
// Console.WriteLine(privateValue); // Error: not accessible, it's private in Base
Console.WriteLine(protectedValue); // Ok: protected IS accessible from an inheriting class
Console.WriteLine(publicValue); // Ok: public, accessible from anywhere
}
}
Derived obj = new Derived();
Console.WriteLine(obj.publicValue); // Ok: public
// Console.WriteLine(obj.protectedValue); // Error: protected is not accessible from outside the hierarchyprotected sits at an intermediate position between private (only the class itself) and
public (the whole program): it's visible to inheriting classes, but stays hidden from the
rest of the code outside that hierarchy. This course will mostly use public and private;
protected will appear occasionally when a base class needs to share something with its
inheritors without exposing it to the rest of the program, and internal won't be needed in
BiblioTech's examples, since the entire project is compiled as a single application.
- The concrete problem:
Available freely modifiable
Available freely modifiableHere's how the Available property was defined until now:
class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
public bool Available { get; set; } = true;
// ...
}With a public { get; set; }, nothing prevents this code, anywhere in the program:
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
book1.Available = true; // Completely bypasses the logic of Lend()/Return()This direct Available = true doesn't go through any check: it doesn't verify whether there
was a loan in progress, it doesn't log any message, it doesn't apply any business rule. The
result is an object whose state (Available) can end up out of sync with the rest of the
system (for example, with the Loan objects that reference that book).
private set: exposing reading, restricting writing
private set: exposing reading, restricting writingThe most direct solution is to allow Available to be read from anywhere
(public get), but only written from within the class itself (private set):
class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
public bool Available { get; private set; } = true;
public LibraryItem(string title, string author)
{
Title = title;
Author = author;
}
public void Lend()
{
if (Available)
{
Available = false; // Ok: we're inside the class itself
Console.WriteLine($"'{Title}' has been lent out.");
}
else
{
Console.WriteLine($"'{Title}' is not available for loan.");
}
}
public void Return()
{
Available = true; // Ok: we're inside the class itself
Console.WriteLine($"'{Title}' has been returned.");
}
}Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(book1.Available); // Ok: public read, True
book1.Lend(); // Ok: changes Available internally, through the method
// book1.Available = true; // Compilation error: the set is private, not accessible from outsideWith private set, the last line no longer compiles: the compiler now guarantees (not just
by convention or good faith) that code outside the class cannot assign a value to Available
without going through Lend() or Return(). This is the simplest and most common way to
encapsulate a property in C#: expose reading, lock writing behind operations with meaning
(Lend, Return), instead of a generic set.
- Properties with validation logic in the
set
setprivate set solves the case of Available, but sometimes something more flexible is
needed: allowing writing from outside, but validating the value before accepting it. That
requires a full property, with an explicit backing field behind it:
class LibraryItem
{
private string _title;
public string Title
{
get { return _title; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("The title cannot be empty.");
}
_title = value;
}
}
// Author, Available, constructor, Lend(), Return() as in the previous section
}Inside the set block, the value keyword automatically represents the value being assigned.
Here, if someone tries to set an empty title or one made up only of whitespace
(string.IsNullOrWhiteSpace), the property throws an ArgumentException — recall exception
handling from Module 2 — instead of accepting invalid data. _title, with a lowercase leading
underscore, is the usual C# convention for naming the private field that backs a property
with its own logic.
Book book1 = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
book1.Title = "";
// Throws ArgumentException: "The title cannot be empty."Auto-implemented property ({ get; set; }) |
Full property with validation | |
|---|---|---|
| Syntax | A single line, no visible field | Explicit get/set, with its own _title field |
| Allows validating the value before accepting it | No | Yes |
| When to use it | The data doesn't need any special restriction | There's a business rule that must always be enforced |
- Read-only properties
A third, stricter level of restriction than private set is to declare no set at all: the
property can only be assigned inside the constructor (or via an initializer), and never again
afterward:
class Member
{
public int Id { get; } // read-only: never changes after the object is created
public string Name { get; set; }
public Member(int id, string name)
{
Id = id;
Name = name;
}
}public int Id { get; } (with no set of any kind) expresses the intent with total clarity:
a member's identifier is set when it's created and must never change during the object's
entire lifetime. Trying to write member1.Id = 5; outside the constructor would be a
compilation error, just as with private set, but here it's clear that not even the class
itself needs to reassign it later.
- Applying encapsulation to the rest of the model:
Member and Loan
Member and LoanWith these three techniques (private set, properties with validation, and read-only
properties), the rest of BiblioTech's model can be revised so each piece of data is exposed
only with the level of control it deserves:
class Loan
{
public Book Book { get; }
public Member Member { get; }
public DateTime LoanDate { get; }
public DateTime? ReturnDate { get; private set; }
public Loan(Book book, Member member)
{
Book = book;
Member = member;
LoanDate = DateTime.Now;
ReturnDate = null;
}
public void RegisterReturn()
{
ReturnDate = DateTime.Now;
}
}Now Book, Member and LoanDate on a Loan are read-only (it doesn't make sense for a
loan to "change" its book or its member once created), and ReturnDate can only be set from
within the Loan class itself, through the RegisterReturn() method — never by assigning the
date directly from outside:
Book book1 = new Book("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Member member1 = new Member(1, "Ana Martinez");
book1.Lend();
Loan loan1 = new Loan(book1, member1);
// loan1.ReturnDate = DateTime.Now; // Compilation error: the set is private
loan1.RegisterReturn(); // correct and only way to register the return
book1.Return();
- The principle of minimal exposure
Everything covered in this lesson boils down to a single practical principle: expose from
your class only the minimum the rest of the program needs to use, and nothing more. Every
public property, method, or field is a promise to the rest of the code: the more you expose,
the more ways the rest of the program will have of coupling itself to your class's internal
details, and the harder it will be to change those details down the road without breaking
something. Before marking something as public, ask yourself whether it truly needs to be, or
whether private would do (or, in the case of properties, restricting just the set).
| Practical rule | Applied in BiblioTech |
|---|---|
| If a piece of data shouldn't change after creation, make it read-only | Member.Id, Loan.Book, Loan.Member, Loan.LoanDate |
If a piece of data should only change through a specific operation, restrict the set |
LibraryItem.Available (only via Lend()/Return()), Loan.ReturnDate (only via RegisterReturn()) |
| If a piece of data must satisfy a rule when assigned, use a full property with validation | LibraryItem.Title |
If a piece of data can change freely with no restriction, a public { get; set; } is enough |
Member.Name |
Common Mistakes and Tips
- Leaving every property as a public
{ get; set; }"just in case": it's the most convenient option in the short term, but it gives up all the protection encapsulation offers; review, property by property, whether it genuinely needs to be freely writable from outside. - Forgetting to initialize a
privatefield with a full property: if you define_titlewith no default value or constructor assignment, theTitleproperty would returnnulluntil the first valid assignment; make sure the constructor always goes through theset(assigningTitle = title;, not_title = title;directly, so validation also applies during construction). - Confusing
private setwith plainprivate:private setstill allows public reading (getremainspublic); only writing is restricted. A field that's fullyprivatewouldn't be accessible even for reading from outside the class. - Using
protectedas a default solution:protectedexposes the member to all current and future inheriting classes, which is also a form of coupling; reserve it for when a base class genuinely needs to share something with its inheritors. - Tip: when designing a new class, start by marking everything as
privateand open up only what the rest of the program genuinely needs, instead of starting with everythingpublicand restricting afterward; it's much easier to open up access later than to close it off without breaking code that already depended on it.
Exercises
-
Modify the
Availableproperty ofLibraryItemso it has a publicgetand a privateset(private set). Check that, after creating aBookobject, the linebook1.Available = true;written outside the class causes a compilation error, whilebook1.Lend()keeps working normally. -
Turn the
Titleproperty ofLibraryIteminto a full property with a private field_title, whosesetthrows anArgumentExceptionwith the message"The title cannot be empty."if the received value is an empty or whitespace-only string (usestring.IsNullOrWhiteSpace). Try assigning an empty title and check that the exception is thrown. -
In the
Loanclass, declareReturnDateasDateTime?with a publicgetand a privateset, and add a methodvoid RegisterReturn()that assigns itDateTime.Now. Create aLoan, check thatReturnDatestarts asnull, and after callingRegisterReturn(), check that it now has a value.
Solutions
class LibraryItem
{
public string Title { get; set; }
public string Author { get; set; }
public bool Available { get; private set; } = true;
public LibraryItem(string title, string author)
{
Title = title;
Author = author;
}
public void Lend()
{
if (Available)
{
Available = false;
}
}
}
// book1.Available = true; // Compilation error: the set is private
private string _title;
public string Title
{
get { return _title; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("The title cannot be empty.");
}
_title = value;
}
}
Running book1.Title = ""; throws an ArgumentException with the message "The title cannot be empty.", since the empty string satisfies string.IsNullOrWhiteSpace.
class Loan
{
public Book Book { get; }
public Member Member { get; }
public DateTime LoanDate { get; }
public DateTime? ReturnDate { get; private set; }
public Loan(Book book, Member member)
{
Book = book;
Member = member;
LoanDate = DateTime.Now;
ReturnDate = null;
}
public void RegisterReturn()
{
ReturnDate = DateTime.Now;
}
}
Loan loan1 = new Loan(new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"), new Member(1, "Ana Martinez"));
Console.WriteLine(loan1.ReturnDate); // (empty / null)
loan1.RegisterReturn();
Console.WriteLine(loan1.ReturnDate); // current date and time
Conclusion
In this lesson you've protected BiblioTech's model's internal state: access modifiers
(public, private, protected, internal), restricting writes with private set, full
properties with validation in the set, and read-only properties, applied to
LibraryItem.Available, LibraryItem.Title and Loan.ReturnDate. It's no longer possible to
leave a book or a loan in an inconsistent state by bypassing business logic: every modification
must go through the methods designed for it.
There's still a conceptual loose end: LibraryItem can still be instantiated directly with
new LibraryItem("...", "..."), even though in practice it doesn't represent any real
BiblioTech material (it only makes sense as a base for Book or Magazine). In the next
lesson, Abstraction, you'll learn to prevent that direct instantiation by turning
LibraryItem into an abstract class, making it clear in the code itself that its only
purpose is to serve as a base for other classes.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
