Introduction
Four parts in, and our coffee shop is running like clockwork. We've overloaded the Barista, inherited through MenuItem, chased virtual dispatch through four-level chains, and locked every class and field down with the right access modifier. There's just one problem — right now, every field in our shop is either wide open to the public or completely sealed off. There's no middle ground. No way to say "you can read the barista's name, but you can't just walk in and rewrite it." No way to run a bit of logic every time someone checks a price.
That's exactly the gap properties fill. A property looks and feels exactly like a plain field from the outside — you read it, you assign to it, business as usual — but underneath, it's actually a pair of methods in disguise, quietly running whatever logic you want, every single time. MSDN puts it precisely: "A property is a member that provides a flexible mechanism to read, write, or compute the value of a private field. Properties can be used as if they are public data members, but they are actually special methods called accessors."
Grab your coffee — we're going deep on get, set, readonly properties, write-only properties, static properties, abstract properties, and how properties behave (strangely!) in inheritance. And since this series started years ago, we'll also stop by the modern C# syntax for properties — the stuff that didn't exist when this series began but you'll use constantly today.
Here's the series roadmap:
Diving in OOP (Properties) — you're here
Note: Every code snippet here has actually been run — no theoretical guessing, just real compiler output (including the errors, especially the errors).
Series Articles
Properties, Explained
Being a C# developer means being handed properties as a gift, honestly. Internally, they're nothing like ordinary variables — a property has no memory of its own. It's really just a specially disguised method (or two) that runs whenever you touch it. A property is a class member, encapsulated and abstracted away from whoever's using it — the caller only ever sees "I'm reading/writing a value," never the logic running behind the scenes.
Let's write some code.
Lab 1: A Property Needs an Accessor
Create a console application named CoffeeShopApp. Add a class called Barista, and try this,
namespace CoffeeShopApp
{
public class Barista
{
public string Name { }
}
}
Output:
Error: 'Barista.Name': property or indexer must have at least one accessor
Self-explanatory — a property needs at least one accessor, a get or a set, something that actually happens when you touch it. Unlike a plain field, a property simply cannot exist empty.
Lab 2: Adding a Get Accessor
namespace CoffeeShopApp
{
public class Barista
{
public string Name
{
get
{
return "I am a Barista's Name property";
}
}
}
}
Barista barista = new Barista();
Console.WriteLine(barista.Name);
Output:
I am a Barista's Name property
That confirms it — our get accessor fired the moment we touched Name.
Get Accessor
Let's add a second property, ExperienceYears, that computes a barista's years of experience from their hire date,
using System;
namespace CoffeeShopApp
{
public class Barista
{
public string Name
{
get { return "I am a Barista's Name property"; }
}
public int ExperienceYears
{
get
{
DateTime hireDate = new DateTime(2018, 01, 20);
DateTime currentDate = DateTime.Now;
int years = currentDate.Year - hireDate.Year;
return years;
}
}
}
}
Barista barista = new Barista();
Console.WriteLine(barista.Name);
Console.WriteLine("Experience: " + barista.ExperienceYears + " years");
Output:
I am a Barista's Name property
Experience: 8 years
Notice what just happened — ExperienceYears runs real logic (subtracting years, referencing DateTime.Now), yet Program.cs, the caller, has zero idea any of that is happening. It just uses the property like a plain value. That's encapsulation, doing exactly what it's meant to do.
Point to Remember: A
getaccessor is only for reading a value. A property with onlygetcannot be assigned to from the caller's side — it's strictly read-only in practice.
Set Accessor
Lab 1: Introducing Set
using System;
namespace CoffeeShopApp
{
public class Barista
{
public string Name
{
get { return "I am a Barista's Name property"; }
}
public int ExperienceYears
{
get
{
DateTime hireDate = new DateTime(2018, 01, 20);
DateTime currentDate = DateTime.Now;
int years = currentDate.Year - hireDate.Year;
Console.WriteLine("Get ExperienceYears called");
return years;
}
set
{
Console.WriteLine("Set ExperienceYears called " + value);
}
}
}
}
Barista barista = new Barista();
Console.WriteLine(barista.Name);
barista.ExperienceYears = 12;
Console.WriteLine("Experience: " + barista.ExperienceYears + " years");
Output:
I am a Barista's Name property
Set ExperienceYears called 12
Get ExperienceYears called
Experience: 8 years
We assigned 12, but reading it back still gives 8 — because our get accessor runs its own fixed calculation logic, completely disconnected from whatever the set accessor did with the value it received. That's a real gap. Let's fix it.
Lab 2: Backing the Property With a Field
using System;
namespace CoffeeShopApp
{
public class Barista
{
private string name;
private int experienceYears;
public string Name
{
get { return name; }
set
{
Console.WriteLine("Set Name called");
name = value;
}
}
public int ExperienceYears
{
get { return experienceYears; }
set
{
Console.WriteLine("Set ExperienceYears called");
experienceYears = value;
}
}
}
}
Barista barista = new Barista();
barista.Name = "Pari";
Console.WriteLine(barista.Name);
barista.ExperienceYears = 12;
Console.WriteLine("Experience: " + barista.ExperienceYears + " years");
Output:
Set Name called
Pari
Set ExperienceYears called
Experience: 12 years
Now the values stick — get returns exactly what set stored, because both accessors share the same private backing field. This is the standard property pattern you'll see in day-to-day code: a public property wrapping a private field.
Point to Remember: The backing field for a property should share the exact same data type as the property itself.
Lab 3: Automatic Properties
If your property doesn't need any custom logic — it's purely a get/set pair over a hidden field — C# lets you skip the backing field entirely,
namespace CoffeeShopApp
{
public class Barista
{
public string Name { get; set; }
public int ExperienceYears { get; set; }
}
}
Barista barista = new Barista();
barista.Name = "Pari";
Console.WriteLine(barista.Name);
barista.ExperienceYears = 12;
Console.WriteLine("Experience: " + barista.ExperienceYears + " years");
Output:
Pari
Experience: 12 years
{ get; set; } is an automatic property — C# quietly generates the hidden backing field for you behind the scenes. No console logging here, obviously, since there's no custom code to run.
Readonly Properties
Give a property only a get, and it becomes effectively read-only from the outside — nobody can assign to it once the object exists.
using System;
namespace CoffeeShopApp
{
public class Barista
{
private string name = "Pari";
private int experienceYears = 8;
public string Name
{
get { return name; }
}
public int ExperienceYears
{
get { return experienceYears; }
}
}
}
Barista barista = new Barista();
barista.Name = "Akhil";
barista.ExperienceYears = 10;
Output:
Error: Property or indexer 'Barista.ExperienceYears' cannot be assigned to -- it is read only
Error: Property or indexer 'Barista.Name' cannot be assigned to -- it is read only
Exactly as expected — with only get defined, any attempt to assign from outside the class fails at compile time.
Write-Only Properties
The mirror image — only a set, no get. Handy for something like a register PIN a barista types in but should never be able to read back out.
namespace CoffeeShopApp
{
public class Barista
{
private string registerPin;
public string RegisterPin
{
set { registerPin = value; }
}
}
}
Barista barista = new Barista();
barista.RegisterPin = "4521";
Console.WriteLine(barista.RegisterPin);
Output:
Error: The property or indexer 'Barista.RegisterPin' cannot be used in this context
because it lacks the get accessor
Setting it works fine; trying to read it back fails immediately, because there's no get accessor to call.
Insight of Properties in C#
Lab 1: You Can't Split a Property in Two
namespace CoffeeShopApp
{
public class Barista
{
private string name;
public string Name
{
set { name = value; }
}
public string Name
{
get { return name; }
}
}
}
Output:
Error: The type 'Barista' already contains a definition for 'Name'
The compiler treats a property name as one single, indivisible unit — you cannot declare get and set for the same name as two separate property blocks.
Lab 2: A Property Can't Share a Name With a Variable
namespace CoffeeShopApp
{
public class Barista
{
private string name;
public string name
{
get { return name; }
set { name = value; }
}
}
}
Output:
Error: The type 'Barista' already contains a definition for 'name'
A property and a field can't share the exact same name and case — the compiler wouldn't be able to tell which one you meant to access.
Properties vs. Variables
There's a common belief that plain variables are always faster than properties. It's not entirely wrong, but it's not the full picture either — a property is really a method call under the hood, and the JIT compiler is often smart enough to inline simple accessors so there's barely any overhead at all. MSDN's own comparison table sums up the real differences well:
| Point of Difference | Variable | Property |
|---|---|---|
| Declaration | Single declaration statement | A block of statements (accessors) |
| Implementation | Single storage location | Executable code (property procedures) |
| Storage | Directly tied to the variable's value | Typically has internal storage not exposed outside the class |
| Executable code | None | Must have at least one accessor |
| Read/write access | Read/write or read-only | Read/write, read-only, or write-only |
| Custom actions on access | Not possible | Fully possible, on get or set |
Static Properties
Just like fields and methods, properties can be static too — accessed through the class name, shared across every barista in the shop.
using System;
namespace CoffeeShopApp
{
public class Barista
{
public static string ShopName
{
set
{
Console.WriteLine("In set ShopName; value is " + value);
}
get
{
Console.WriteLine("In get ShopName");
return "The Byte-Sized Café";
}
}
}
}
Barista.ShopName = "The Byte-Sized Café Downtown";
Console.WriteLine(Barista.ShopName);
Output:
In set ShopName; value is The Byte-Sized Café Downtown
In get ShopName
The Byte-Sized Café
Static properties behave exactly like static fields or methods — accessed via the class name, never through an instance.
Property Return Type Rules
Lab 1: No Void Properties
namespace CoffeeShopApp
{
public class Barista
{
public void PrepareOrder
{
get
{
Console.WriteLine("Get called");
}
}
}
}
Output:
Error: 'PrepareOrder': property or indexer cannot have void type
Point to Remember: A property can never have a
voidreturn type.
Lab 2: Set Can't Return a Value
namespace CoffeeShopApp
{
public class Barista
{
public int ExperienceYears
{
set { return 5; }
}
}
}
Output:
Error: Since 'Barista.ExperienceYears.set' returns void, a return keyword
must not be followed by an object expression
The compiler treats set as an implicit void method that takes a parameter — a bare return; compiles fine, but return 5; doesn't, because set was never designed to hand anything back.
The value Keyword
value is reserved inside a set accessor — try declaring a local variable with that exact name, and things go sideways,
namespace CoffeeShopApp
{
public class Barista
{
public string Name
{
set { string value; }
}
}
}
Output:
Error: A local variable named 'value' cannot be declared in this scope because it would
give a different meaning to 'value', which is already used in a 'parent or current'
scope to denote something else
value implicitly represents whatever was assigned to the property — you never declare it, you just use it.
Abstract Properties
Yes — properties can be abstract too, and they follow the same override rules as abstract methods from Part 3.
Lab 1: Overriding Both Accessors
using System;
namespace CoffeeShopApp
{
public abstract class MenuItem
{
public abstract decimal Price { get; set; }
}
public class SpecialMenuItem : MenuItem
{
public override decimal Price
{
get
{
Console.WriteLine("Get Price called");
return 250;
}
set
{
Console.WriteLine("Set Price called, value is " + value);
}
}
}
}
SpecialMenuItem item = new SpecialMenuItem();
item.Price = 300;
Console.WriteLine(item.Price);
Output:
Set Price called, value is 300
Get Price called
250
MenuItem's abstract Price has no body at all for either accessor — neither get nor set — so SpecialMenuItem is required to implement both, marked override.
Point to Remember: If a derived class doesn't explicitly mark a property
override, it's treated asnewby default — the same hiding behavior we saw with methods back in Part 3.
Lab 2: You Can't Override an Accessor That Was Never There
using System;
namespace CoffeeShopApp
{
public abstract class MenuItem
{
public abstract decimal Price { get; }
}
public class SpecialMenuItem : MenuItem
{
public override decimal Price
{
get
{
Console.WriteLine("Get Price called");
return 250;
}
set
{
Console.WriteLine("Set Price called, value is " + value);
}
}
}
}
Output:
Error: 'SpecialMenuItem.Price.set': cannot override because 'MenuItem.Price'
does not have an overridable set accessor
Since the base class only declared a get, there's simply nothing for set to override.
Point to Remember: You can only override an accessor that actually exists in the base class's abstract property.
Properties in Inheritance — A Genuine Gotcha
namespace CoffeeShopApp
{
public class MenuItemBase
{
public int Price
{
set { }
}
}
public class SpecialMenuItem : MenuItemBase
{
public int Price
{
get { return 250; }
}
}
}
MenuItemBase baseItem = new MenuItemBase();
baseItem.Price = 100;
SpecialMenuItem specialItem = new SpecialMenuItem();
((MenuItemBase)specialItem).Price = 150;
specialItem.Price = 100;
It's tempting to think C# will merge these into one combined property — set from the base, get from the derived class. It doesn't. The compiler treats them as two completely independent properties; the one in SpecialMenuItem simply hides the one in MenuItemBase — exactly the same hiding behavior methods showed back in Part 3.
Output:
Error: Property or indexer 'SpecialMenuItem.Price' cannot be assigned to -- it is read only
((MenuItemBase)specialItem).Price = 150; compiles fine — casting up to the base class reaches the base's set. But specialItem.Price = 100; fails, because the derived class's own Price only ever declared a get.
Properties: The Modern C# Syntax
Everything above is the classic, foundational syntax — and it's still exactly how properties work under the hood today. But C# has added a lot of shorthand over the years that makes properties faster to write and, honestly, harder to misuse. Since we're building a real coffee shop here, let's see the modern equivalents side by side.
Expression-Bodied Properties (C# 6)
For a get-only property that's just one expression, you can drop the braces and return entirely,
public class Barista
{
private string name;
public string Greeting => "Welcome, " + name + "!";
}
=> here means exactly the same thing as get { return "Welcome, " + name + "!"; } — just far less to type.
Auto-Property Initializers (C# 6)
You can give an automatic property a default value right at declaration, no constructor required,
public class MenuItem
{
public string Name { get; set; } = "House Blend";
public decimal Price { get; set; } = 199;
}
Get-Only Auto Properties (C# 6)
Want a readonly automatic property that can still be set once, from inside a constructor? This wasn't possible with plain { get; } before C# 6,
public class Barista
{
public string Name { get; }
public Barista(string name)
{
Name = name;
}
}
Name behaves exactly like our manual readonly property from earlier, but with none of the backing-field boilerplate.
Init-Only Setters (C# 9)
init lets a property be set during object initialization — and then it locks, permanently, just like readonly,
public class MenuItem
{
public string Name { get; init; }
public decimal Price { get; init; }
}
var latte = new MenuItem { Name = "Latte", Price = 250 };
// latte.Price = 300; // compile-time error — init-only, can't be reassigned
This is genuinely useful for something like a MenuItem, which really shouldn't have its price silently mutated once created — you get the readability of object initializer syntax with the safety of a readonly field.
Required Members (C# 11)
required forces the caller to set a property during initialization, or the code simply won't compile,
public class MenuItem
{
public required string Name { get; init; }
public required decimal Price { get; init; }
}
var espresso = new MenuItem { Name = "Espresso", Price = 150 }; // fine
// var invalid = new MenuItem(); // compile-time error — Name and Price are required
No more forgetting to set a field and only discovering it's null at run time — required catches it right at the call site.
Primary Constructors (C# 12)
The newest shorthand — you can now fold constructor parameters straight into the class declaration, and reference them directly in property definitions,
public class Barista(string name, int experienceYears)
{
public string Name { get; } = name;
public int ExperienceYears { get; } = experienceYears;
}
var barista = new Barista("Pari", 8);
Console.WriteLine(barista.Name);
Output:
Pari
No explicit constructor body needed at all — the primary constructor parameters are available directly wherever you'd normally reference this.name.
Point to Remember: The classic
get/setaccessor rules from earlier in this article still apply underneath every one of these modern forms —initis just a restrictedset, and expression-bodied syntax is just a compactget. Knowing the fundamentals means none of the new syntax ever feels like magic.
What's New in C# 14
C# 14 (shipping with .NET 10) brings the biggest change to property syntax since automatic properties themselves — plus a couple of smaller, genuinely useful additions. Let's put each one to work in the coffee shop.
The field Keyword — Semi-Automatic Properties
This is the headline feature. Until now, the moment you needed even a tiny bit of custom logic in a property — validation, logging, a side effect — you had to abandon automatic properties entirely and write out a full backing field by hand, exactly like our Barista.Name/Barista.ExperienceYears example earlier in this article.
C# 14 introduces the contextual field keyword, which refers to the property's compiler-generated backing field directly, without you ever having to declare it,
public class Barista
{
public string Name
{
get => field;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
public decimal HourlyRate
{
get;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Hourly rate can't be negative");
field = value;
}
}
}
var barista = new Barista { Name = "Pari", HourlyRate = 250 };
Console.WriteLine(barista.Name);
barista.HourlyRate = -50; // throws ArgumentOutOfRangeException at run time
Output:
Pari
Unhandled exception: System.ArgumentOutOfRangeException: Hourly rate can't be negative
Notice HourlyRate only defines a custom set — the get; on its own is enough, and field inside set automatically refers to the same hidden storage. This is exactly the middle ground developers have wanted for years: full validation logic, zero manual backing-field boilerplate.
Point to Remember:
fieldis a contextual keyword — it only means "the compiler-generated backing field" inside a property accessor. Outside a property,fieldis still perfectly usable as an ordinary identifier name, so nothing existing breaks.
Partial Properties
C# 13 introduced partial properties, and C# 14 rounds out the feature — useful when a property's declaration and its implementation need to live in separate files, which source generators (like ones used for validation frameworks or ORMs) rely on heavily.
// MenuItem.cs — the declaring part
public partial class MenuItem
{
public partial string Name { get; set; }
}
// MenuItem.Generated.cs — the implementing part
public partial class MenuItem
{
private string _name = "House Blend";
public partial string Name
{
get => _name;
set => _name = value;
}
}
Handy if you've ever used a code generator that needed to attach behavior to a property without touching the file where you declared it — the two halves compile into a single property, same as partial methods and partial classes have worked for years.
Null-Conditional Assignment
A small but genuinely satisfying quality-of-life addition — you can now use ?. on the left-hand side of an assignment, not just when reading a value,
MenuItem? featuredItem = GetTodaysFeaturedItem(); // might return null
featuredItem?.Price = 199;
Before C# 14, that line had to be written as a full if (featuredItem != null) { featuredItem.Price = 199; }. Now the null check and the assignment collapse into one line — and if featuredItem is null, the assignment simply doesn't happen, no exception, no extra branching.
Point to Remember: Null-conditional assignment only skips the assignment when the left-hand side is
null— it does not swallow exceptions thrown by anything on the right-hand side of the expression.
Summary
Let's lock in everything we covered today,
A property must have at least one accessor —
get,set, or both.A property's backing field should share the same data type as the property.
A property can never have a
voidreturn type.setis implicitly void — it can't return a value.valueis a reserved word insidesetand cannot be redeclared as a local variable.A property and a field cannot share the same name and case.
A property cannot be split into two separately declared blocks under the same name.
If a derived class property isn't explicitly marked
override, it's treated asnewby default.You can only override an accessor that's actually declared in the base class's abstract property.
A
get-only property is effectively read-only from the outside; aset-only property is write-only.A base class property and a derived class property with the same name are treated as entirely independent — the derived one hides the base one.
Modern C# adds expression-bodied properties, auto-property initializers, get-only auto properties,
initaccessors,requiredmembers, and primary constructors — all shorthand over the exact sameget/setfundamentals.C# 14 adds the
fieldkeyword for semi-automatic properties (custom logic with no manual backing field), partial properties, and null-conditional assignment (?.=).
Conclusion
That's properties, top to bottom — from the very first "why won't this compile without an accessor" error, all the way to required members and primary constructors in modern C#. Properties are the reason your coffee shop's MenuItem can look like a plain, friendly object from the outside while quietly enforcing every rule you want underneath — and now you know exactly what's happening on both sides of that curtain.
Next up in the series: Indexers — how to make your own classes behave like arrays, so you can write menu[0] and have it just work.
Keep coding, keep learning, and don't forget your coffee. ☕

Join the conversation! Your thoughts help the community grow.