ZetCode

C# access modifier

last modified October 18, 2026

In this article we show how to control the visibility of types and their members in C#.

Access modifiers set the accessibility of types and of their members, such as fields, properties, and methods. They let a type hide its internal details and expose only a well-defined interface. This protects data against accidental modifications and makes programs more robust; it is the basis of encapsulation.

C# has four access modifiers: public, protected, internal, and private. They can be combined into protected internal and private protected, which gives six accessibility levels:

In addition, C# 11 added the file modifier, which limits a type to the source file in which it is declared.

Modifier Class itself Same assembly Other assembly
Derived Other Derived Other
publicyesyesyesyesyes
protected internalyesyesyesyesno
protectedyesyesnoyesno
internalyesyesyesnono
private protectedyesyesnonono
privateyesnononono

The table summarizes the C# access modifiers, from the least to the most restrictive. Derived means code in a class derived from the class, Other means code in any other class.

C# default accessibility

If we do not specify an access modifier, the most restrictive sensible default is used. Members of classes and structs are private by default. Top-level types, which are not nested in another type, are internal by default; they can only be public or internal (or file). Interface members are public by default.

Program.cs
var c = new Counter();
c.Increment();
c.Increment();

Console.WriteLine(c.Value);
// Console.WriteLine(c.count); error CS0122

class Counter
{
    int count;

    public int Value => count;

    public void Increment() => count++;
}

The count field has no access modifier, so it is private. The Counter class itself is internal.

// Console.WriteLine(c.count); error CS0122

Accessing the private field from outside the class would cause the CS0122 error: the field is inaccessible due to its protection level.

$ dotnet run
2

C# public and private

In the following example, we use the public and private access modifiers.

Program.cs
var p = new Person("Jane");
p.SetAge(17);

Console.WriteLine($"{p.Name} is {p.GetAge()} years old");

p.SetAge(-5);

class Person(string name)
{
    public string Name = name;
    private int age;

    public int GetAge()
    {
        return age;
    }

    public void SetAge(int age)
    {
        if (age < 0)
        {
            Console.WriteLine("Age cannot be negative");
            return;
        }

        this.age = age;
    }
}

In the above program, we have two member fields. One is declared public, the other private.

public string Name = name;
private int age;

The Name field is public; any code can read and change it. The age field is private; it can be used only inside the Person class.

public void SetAge(int age)
{
    if (age < 0)
    {
        Console.WriteLine("Age cannot be negative");
        return;
    }

    this.age = age;
}

If a member field is private, the only way to access it from the outside is through the public members of the class. The SetAge method checks the value before it changes the field, so the age can never be negative. This is the main benefit of hiding data.

var p = new Person("Jane");
p.SetAge(17);

We create a new instance of the Person class and set the age through the public method.

$ dotnet run
Jane is 17 years old
Age cannot be negative

Public fields are shown here for simplicity. In real code, data is exposed through properties.

C# accessor accessibility

The get and set accessors of a property can have different accessibility. A common pattern is a property that everyone can read but only the class can change.

Program.cs
var acc = new Account();

acc.Deposit(100);
acc.Deposit(50);

Console.WriteLine($"Balance: {acc.Balance}");
Console.WriteLine($"Transactions: {acc.Transactions}");

// acc.Balance = 1000; error CS0272

class Account
{
    public decimal Balance { get; private set; }
    public int Transactions { get; private set; }

    public void Deposit(decimal amount)
    {
        Balance += amount;
        Transactions++;
    }
}

The balance of the account can be changed only through deposits.

public decimal Balance { get; private set; }

The property is public, but its setter is private. The accessor modifier must be more restrictive than the property itself.

// acc.Balance = 1000; error CS0272

Assigning to the property from outside the class would cause the CS0272 error, because the set accessor is inaccessible.

$ dotnet run
Balance: 150
Transactions: 2

C# private members and inheritance

A derived class inherits all members of its base class, including the private ones; they are part of every derived object. However, private members are not accessible in the derived class. Public and protected members are.

Program.cs
var derived = new Derived();
derived.Info();

class Base
{
    public string name = "Base";
    protected int id = 5323;
    private bool isDefined = true;

    public bool IsDefined() => isDefined;
}

class Derived : Base
{
    public void Info()
    {
        Console.WriteLine("This is Derived class");
        Console.WriteLine(name);
        Console.WriteLine(id);
        // Console.WriteLine(isDefined); error CS0122
        Console.WriteLine(IsDefined());
    }
}

In the preceding program, we have a Derived class which inherits from the Base class. The Base class has three member fields, all with different access modifiers.

class Derived : Base

The class Derived inherits from the Base class. The base class is specified after a colon.

Console.WriteLine(name);
Console.WriteLine(id);
// Console.WriteLine(isDefined); error CS0122
Console.WriteLine(IsDefined());

The public and the protected members can be accessed in the Derived class. The private field cannot be accessed directly; if we uncommented the line, the code would not compile. The derived class can still use the field indirectly, through the public IsDefined method of the base class.

$ dotnet run
This is Derived class
Base
5323
True

C# access across assemblies

The internal, protected internal, and private protected modifiers depend on assemblies, so we need two projects to see them in action. We create a class library and a console application that uses it.

$ mkdir Access
$ cd Access
$ dotnet new sln
$ dotnet new classlib -o Shapes
$ dotnet new console -o App
$ dotnet sln add Shapes/Shapes.csproj App/App.csproj
$ dotnet add App reference Shapes/Shapes.csproj

The Shapes library and the App application are compiled into two separate assemblies. We delete the generated Class1.cs file.

Shapes/Shape.cs
namespace Shapes;

public class Shape
{
    public string PublicName = "public";
    protected string ProtectedName = "protected";
    internal string InternalName = "internal";
    protected internal string ProtectedInternalName = "protected internal";
    private protected string PrivateProtectedName = "private protected";
    private string privateName = "private";

    public string Describe() => $"Shape with a {privateName} field";
}

The Shape class has a field for each accessibility level.

Shapes/Square.cs
namespace Shapes;

public class Square : Shape
{
    public void Show()
    {
        Console.WriteLine("Square (derived, same assembly):");
        Console.WriteLine($"  {PublicName}");
        Console.WriteLine($"  {ProtectedName}");
        Console.WriteLine($"  {InternalName}");
        Console.WriteLine($"  {ProtectedInternalName}");
        Console.WriteLine($"  {PrivateProtectedName}");
    }
}

The Square class derives from Shape in the same assembly. It can access all fields except the private one.

App/Program.cs
using Shapes;

var shape = new Shape();

Console.WriteLine("Program (not derived, other assembly):");
Console.WriteLine($"  {shape.PublicName}");
// Console.WriteLine(shape.InternalName); error CS1061
// Console.WriteLine(shape.ProtectedInternalName); error CS0122
Console.WriteLine($"  {shape.Describe()}");

new Square().Show();
new Circle().Show();

class Circle : Shape
{
    public void Show()
    {
        Console.WriteLine("Circle (derived, other assembly):");
        Console.WriteLine($"  {PublicName}");
        Console.WriteLine($"  {ProtectedName}");
        Console.WriteLine($"  {ProtectedInternalName}");
        // Console.WriteLine(InternalName); error CS0103
        // Console.WriteLine(PrivateProtectedName); error CS0103
    }
}

In the application, we use the Shape class directly and through the derived Circle class.

Console.WriteLine($"  {shape.PublicName}");
// Console.WriteLine(shape.InternalName); error CS1061
// Console.WriteLine(shape.ProtectedInternalName); error CS0122

Code in another assembly that does not derive from Shape can use only the public members. Internal members are not visible to other assemblies at all, so the compiler reports that the class does not contain the member.

Console.WriteLine($"  {ProtectedName}");
Console.WriteLine($"  {ProtectedInternalName}");
// Console.WriteLine(InternalName); error CS0103
// Console.WriteLine(PrivateProtectedName); error CS0103

A derived class in another assembly can access the protected and protected internal members, but not the internal and private protected ones.

$ dotnet run --project App
Program (not derived, other assembly):
  public
  Shape with a private field
Square (derived, same assembly):
  public
  protected
  internal
  protected internal
  private protected
Circle (derived, other assembly):
  public
  protected
  protected internal

The output matches the table at the beginning of the article. An assembly can share its internal members with chosen other assemblies, typically with its test project, using the InternalsVisibleTo attribute.

C# file access modifier

The file modifier, added in C# 11, restricts a type to the source file in which it is declared. It is useful for helper types that should not be visible anywhere else, and source generators use it to avoid name conflicts.

Report.cs
namespace FileEx;

public static class Report
{
    public static string Create(string title) => Formatter.Underline(title);
}

file static class Formatter
{
    public static string Underline(string text) =>
        $"{text}\n{new string('-', text.Length)}";
}

The Formatter class is visible only in the Report.cs file. The public Report class uses it internally.

Program.cs
using FileEx;

Console.WriteLine(Report.Create("Monthly report"));

// Console.WriteLine(Formatter.Underline("test")); error CS0117

class Formatter
{
}

In Program.cs, we declare another class named Formatter. There is no conflict, because the file-local class is invisible here. The Formatter name in this file refers to our empty class, so calling Underline would cause the CS0117 error.

$ dotnet run
Monthly report
--------------

Source

Access Modifiers - programming guide

In this article we worked with C# access modifiers.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.