Access Modifiers
public, internal, private, protected and the two combinations, what each one lets in, and why private is the right starting point.
private int _count; // or public, internal, protected, protected internal, private protectedAn access modifier is a word in front of a type or a member saying which code may use it. It separates the part of your design others may depend on from the part you can change tomorrow without telling anyone. An assembly is one compiled output of one project: a single .dll or .exe file.
The six levels
| Modifier | Who may use it |
|---|---|
public | Any code anywhere. |
internal | Code in the same assembly. |
private | Code in the same type. |
protected | The same type, and types built on top of it. |
protected internal | The same assembly, or types built on top of it anywhere. |
private protected | Types built on top of it, in the same assembly only. |
A type built on top of another is a derived class, which Stage 3 covers. The last three only matter once you have one.
Defaults
Leave the modifier off a class member and it is private. Leave it off a top-level type and it is internal. Neither default is public, deliberately.
Which to pick
Start at private and widen only when something outside genuinely needs in. Every public member is a promise to callers you cannot see. internal is the useful middle. C# 11 added a seventh option for top-level types only, file, which limits a type to its own source file and cannot be combined with any access modifier.
Example
Till till = new Till();
till.Add(250);
till.Add(125);
Console.WriteLine($"total: {till.Total()}");
// Neither of these compiles, which is the whole point:
// till._cents = 0; // _cents is private
// till.Audit(); // Audit has no modifier, so it is private too
// A top-level type with no modifier is internal: this assembly only.
class Till
{
// Members default to private. Writing the word is clearer than relying
// on the default, and that is the usual house style.
private int _cents;
public void Add(int cents)
{
_cents = _cents + cents;
Audit();
}
public int Total()
{
return _cents;
}
// No modifier, so private. Only code inside Till can call it.
void Audit()
{
if (_cents < 0)
{
Console.WriteLine("negative till");
}
}
}
// Output:
// total: 375When to use it
- Keep a class's data private so every change to it goes through a method that can validate it.
- Share a helper across your own project with internal while keeping it out of the published API.
- Let a test project reach internal members without making them public.
More examples
All six levels on one class
Worth reading once to fix the two awkward names in memory. private protected is the narrower of the pair, being the overlap of private-ish and protected; protected internal is the wider, being the union. If you cannot remember which is which, you almost certainly want protected or internal on its own, and clarity beats cleverness here.
namespace Payments;
public class Gateway
{
// Visible to every project that references this one.
public string Name { get; init; } = "sandbox";
// This assembly only. Useful for something your own code and tests
// need but no consumer should see.
internal int RetryLimit { get; set; } = 3;
// Gateway itself and nothing else.
private int _attempts;
// Gateway, plus any class built on top of it. A class built on top of
// another is a derived class; Stage 3 covers writing one.
protected int Attempts => _attempts;
// Derived classes, but only ones inside this assembly.
private protected void ResetAttempts()
{
_attempts = 0;
}
// Anything in this assembly, or a derived class in any assembly.
protected internal bool IsSandbox()
{
return Name == "sandbox";
}
}The file modifier, C# 11
This exists mainly for source generators, which add files to someone else's project and must not collide with a name the author already used. In hand-written code it is occasionally handy for a helper that genuinely belongs to one file. Note it applies only to top-level types: you cannot put file on a member or on a nested type.
// Parser.cs
namespace Tooling;
// Visible only inside this one source file. Another file in the same
// project cannot see TokenNames at all, and `file` cannot be combined
// with public, internal, protected or private.
file static class TokenNames
{
public const string Number = "number";
public const string Word = "word";
}
internal class Parser
{
public string Classify(string text)
{
return text.Length > 0 && char.IsDigit(text[0])
? TokenNames.Number
: TokenNames.Word;
}
}Opening internals to a test project
Reach for this when a test needs to reach past the public surface without you making that surface wider for everyone. Use it sparingly: tests that only touch public members keep working when you reorganise the inside, and tests wired into internals do not. It is also not a security control, since reflection can read private members at run time regardless.
// PriceList.cs in the library project
using System.Runtime.CompilerServices;
// Grants the named assembly the same access as this one's own code.
// Assembly-level attributes must appear before any namespace or type.
[assembly: InternalsVisibleTo("Payments.Tests")]
namespace Payments;
public class PriceList
{
// internal, so tests can set it directly while consumers cannot.
internal int BaseCents { get; set; } = 1000;
public int QuoteCents(int quantity)
{
return BaseCents * quantity;
}
}
Discussion