Skip to main content

static, sealed and partial: Three Class Modifiers – C# | Lesson 42

Published / updated

Introduction​

Since Lesson 34, SchoolRules has been a static class, with a promise to explain it later. This lesson keeps that promise, and covers two more words that change how a class can be used: sealed and partial.

The three are small keywords with big effects:

  • static decides whether you need an object at all.
  • sealed decides whether anyone may inherit.
  • partial lets one class live in several files.

By the end of this lesson, you will be able to:

  • Explain static and instance members, and write a static class.
  • Choose between const and static readonly.
  • Explain why shared static state is dangerous in a web application.
  • Seal a class, and say why you would.
  • Split a class across files with partial, as code generators do.

Why this matters​

static is one of the most misused keywords in C#. Used for pure rules, such as the grade scale, it is perfect. Used to store data, such as a running counter, it quietly breaks the moment many users share the application, which is exactly what a Web API is for.

partial matters in Module 9: Entity Framework Core generates classes from SchoolDb, and you add your own code in a separate partial file. When the database changes, the generator rewrites its file and leaves yours alone.

The concept​

Example: static and instance members​

Instance memberStatic member
Belongs toOne objectThe class itself
Called asdiya.GetAgeOn(...)SchoolRules.GetGrade(...)
How many copiesOne per objectExactly one, shared by everyone
Can use this object's dataYesNo: there is no object

Think of the school's notice board and the students' diaries. Every student has their own diary (instance): what is written in one is not in another. There is one notice board (static): everyone reads the same notice, and a change is seen by all.

Example: a static class​

A static class can contain only static members, and can never be used to create an object. It suits code that needs no stored data of its own: calculations, rules, conversions.

Static methods are also easy to test: the same input always gives the same output, with no setup and no hidden state. GetGrade(92m) returns "A1" every time, in any test, on any machine.

SchoolRules.GetGrade(92m) takes a percentage and returns a grade. Nothing about a particular object is involved, so a static method is the honest design. Math.Round and Console.WriteLine are static methods for the same reason.

Example: static members in normal classes​

A class does not have to be static to have static members. You have used many types that mix both:

TypeStatic memberInstance member
intint.Parse("45500")mark.ToString()
stringstring.Join(", ", names)name.Trim()
DateOnlyDateOnly.FromDateTime(DateTime.Now)date.AddDays(14)

The static ones create or combine values; the instance ones work on one value you already have. The same split is sensible in your own classes.

Example: const or static readonly?​

conststatic readonly
Value fixedWhen the code is compiledOnce, when the program first uses the class
Allowed typesNumbers, bool, char, stringAny type, such as DateOnly
SMS examplePassMark = 33YearStart = new DateOnly(2026, 6, 1)

public const DateOnly YearStart = ... fails with error CS0283: The type 'DateOnly' cannot be declared const, because a DateOnly is created at run time. Use static readonly for such values.

Example: sealed classes​

sealed means no class may inherit from this one. Trying fails with error CS0509: 'PrefectStudent': cannot derive from sealed type 'Student'.

Seal a class when it was not designed to be extended: its rules could break if a derived class overrode parts of it. Sealing also lets .NET make calls slightly faster, because no override can exist. Abstract classes such as Person are never sealed; they exist only to be inherited.

Example: partial classes​

partial lets you write one class in several files. At build time, the compiler joins the parts into a single class. Every part must say partial, and all parts must be in the same project and namespace.

The main use is generated code. A tool writes one file, you write the other, and regenerating never deletes your work.

Apply it to the SMS​

Step 1: SchoolRules moves into Core​

The rules were written in the console project in Lesson 34. The Web API needs them too, so they move to SchoolApp.Core/Rules, as a public static class in a namespace:

Show src/SchoolApp.Core/Rules/SchoolRules.cs
src/SchoolApp.Core/Rules/SchoolRules.cs
using SchoolApp.Core.Exceptions;

namespace SchoolApp.Core.Rules;

// Lesson 34: school rules as reusable methods (first written in the console project).
// Lesson 42: moved into Core so every project can use them. A static class has no objects:
// you call its members on the class name, such as SchoolRules.GetGrade(92m).
public static class SchoolRules
{
public const int PassMark = 33; // const: fixed when the code is compiled

// Grade for a percentage: the same scale as fn_GetGrade in SQL (Lesson 21).
public static string GetGrade(decimal percentage)
{
return percentage switch
{
>= 91 => "A1",
>= 81 => "A2",
>= 71 => "B1",
>= 61 => "B2",
>= 51 => "C1",
>= 41 => "C2",
>= 33 => "D",
_ => "E"
};
}

// Lesson 35: an absent student has no mark (NULL), so no grade, like fn_GetGrade(NULL).
public static string? GetGrade(decimal? percentage)
{
if (percentage is null)
{
return null;
}
return GetGrade(percentage.Value);
}

public static bool HasPassed(decimal percentage) => percentage >= PassMark;

public static decimal CalculatePercentage(decimal marksObtained, decimal maxMarks = 100m)
{
return Math.Round(marksObtained / maxMarks * 100, 2);
}

public static decimal CalculateAttendancePercentage(int attendedDays, int markedDays)
{
if (markedDays == 0)
{
return 0;
}
return Math.Round(100m * attendedDays / markedDays, 2);
}

public static decimal CalculateAttendancePercentage(string week)
{
int attended = 0;
foreach (char day in week)
{
if (day != 'A')
{
attended++;
}
}
return CalculateAttendancePercentage(attended, week.Length);
}

public static bool TryParseAdmissionNumber(string text, out int year, out int serial)
{
year = 0;
serial = 0;
string[] parts = text.Split('-');
return parts.Length == 3
&& parts[0].Equals("ADM", StringComparison.OrdinalIgnoreCase)
&& int.TryParse(parts[1], out year)
&& int.TryParse(parts[2], out serial);
}

// Lesson 42: now throws the school's own InvalidMarkException (Lesson 39), error 50001.
public static void ValidateMark(decimal marksObtained, decimal maxMarks)
{
if (marksObtained < 0 || marksObtained > maxMarks)
{
throw new InvalidMarkException(marksObtained, maxMarks);
}
}

public static decimal Average(params decimal[] marks)
{
if (marks.Length == 0)
{
return 0;
}

decimal total = 0;
foreach (decimal mark in marks)
{
total += mark;
}
return Math.Round(total / marks.Length, 2);
}
}

Two things changed along the way. ValidateMark now throws the school's own InvalidMarkException from Lesson 39, and ApplyPayment is gone, because FeeInvoice.RecordPayment from Lesson 38 does that job properly.

Step 2: a partial, sealed Student​

Student is now in two files: the data in Student.cs, the behaviour in Student.Behaviour.cs. It is also sealed, like Teacher, Guardian, and the two senders.

src/SchoolApp.Core/Entities/Student.cs
namespace SchoolApp.Core.Entities;

// Lesson 37: a class describes what every student has (properties) and can do (methods).
// Lesson 38: names are checked and trimmed; the admission number can never change.
// Lesson 39: names, Id, and FullName now come from the Person base class.
// Lesson 42: sealed (no class may inherit from Student) and partial: this file holds the data,
// Student.Behaviour.cs holds the methods. Both parts make one class.
public sealed partial class Student : Person
{
public string AdmissionNumber { get; } // set once, in the constructor
public DateOnly DateOfBirth { get; set; }

// base(...) runs Person's constructor first, which checks the names.
public Student(string admissionNumber, string firstName, string lastName, DateOnly dateOfBirth)
: base(firstName, lastName)
{
AdmissionNumber = RequireText(admissionNumber, nameof(AdmissionNumber));
DateOfBirth = dateOfBirth;
}
}
src/SchoolApp.Core/Entities/Student.Behaviour.cs
namespace SchoolApp.Core.Entities;

// Lesson 42: the second part of the partial Student class: behaviour only.
public partial class Student
{
public override string Role => "Student";

// Lesson 40: override replaces Person's Describe; base.Describe() reuses it.
public override string Describe() => $"{base.Describe()} (student {AdmissionNumber})";

// The age on a given date, the same rule as Lesson 29's SQL task.
public int GetAgeOn(DateOnly date)
{
int age = date.Year - DateOfBirth.Year;
if (DateOfBirth > date.AddYears(-age))
{
age--; // the birthday has not happened yet in that year
}
return age;
}
}

Step 3: the program​

Show src/SchoolApp.Console/Program.cs
src/SchoolApp.Console/Program.cs
// Lesson 42: static, sealed, and partial.
using SchoolApp.Core.Entities;
using SchoolApp.Core.Exceptions;
using SchoolApp.Core.Rules;

// 1. A static class: call members on the class name. No object is ever created.
Console.WriteLine($"Pass mark {SchoolRules.PassMark}; 92% is {SchoolRules.GetGrade(92m)}; 32% passed: {SchoolRules.HasPassed(32m)}");

try
{
SchoolRules.ValidateMark(105m, 100m);
}
catch (InvalidMarkException ex)
{
Console.WriteLine($"Error {ex.ErrorNumber}: {ex.Message}");
}

// 2. const versus static readonly.
Console.WriteLine($"School days per week: {SchoolCalendar.SchoolDaysPerWeek}; year starts {SchoolCalendar.YearStart:yyyy-MM-dd}");

// 3. Static fields are shared by everyone: two "users" share one counter.
string forDiya = DemoReceiptCounter.Next();
string forKabir = DemoReceiptCounter.Next();
Console.WriteLine($"Receipts: {forDiya}, {forKabir}; issued so far: {DemoReceiptCounter.Issued}");

// 4. partial: Student is written in two files, but it is one class.
Student diya = new Student("ADM-2017-0002", "Diya", "Patel", new DateOnly(2011, 8, 23));
Console.WriteLine($"{diya.Describe()}, age {diya.GetAgeOn(new DateOnly(2026, 7, 1))} on 2026-07-01");

// 5. sealed: nothing can inherit from Student, Teacher, or Guardian.
Console.WriteLine($"Student is sealed: {typeof(Student).IsSealed}; Person is sealed: {typeof(Person).IsSealed}");

static class SchoolCalendar
{
public const int SchoolDaysPerWeek = 5; // compile-time value
public static readonly DateOnly YearStart = new DateOnly(2026, 6, 1); // set once, at run time
}

// A bad idea on a server, shown on purpose: every user of the app shares this one counter,
// and it starts again at 1 whenever the app restarts. SchoolDb uses a SEQUENCE instead (Lesson 22).
static class DemoReceiptCounter
{
private static int _issued;

public static int Issued => _issued;

public static string Next()
{
_issued++;
return $"DEMO-{_issued:D6}";
}
}
Output
Pass mark 33; 92% is A1; 32% passed: False
Error 50001: Marks must be between 0 and 100; got 105.
School days per week: 5; year starts 2026-06-01
Receipts: DEMO-000001, DEMO-000002; issued so far: 2
Diya Patel (student ADM-2017-0002), age 14 on 2026-07-01
Student is sealed: True; Person is sealed: False

Example: no objects of a static class​

new SchoolRules() fails with two errors: error CS0712: Cannot create an instance of the static class 'SchoolRules' and error CS0723: Cannot declare a variable of static type 'SchoolRules'.

A static class may not even hold instance members: public int Days = 5; inside one fails with error CS0708: cannot declare instance members in a static class. The compiler makes sure the class stays a pure toolbox.

Example: shared static state​

DemoReceiptCounter keeps a static field, _issued. Diya's receipt gets 1 and Kabir's gets 2, because both calls change the same single field.

In a console app with one user, that looks fine. In a Web API, one copy of the app serves every parent at once, so every request shares that one field. Two requests in the same instant can both read 0 and both write 1: two receipts with the same number.

Example: why the database owns the counter​

It gets worse. The counter starts again at 1 every time the app restarts, and a busy school runs two or more copies of the API, each with its own counter.

ProblemStatic counter in C#ReceiptNumbers SEQUENCE in SchoolDb (Lesson 22)
Two requests at onceCan duplicateNever duplicates
App restartStarts again at 1Continues
Several API copiesOne counter eachOne shared counter

That is why Lesson 22 used a SQL sequence. Rule of thumb: static members are for rules and constants, never for data that belongs to users or must survive a restart. The demo class stays in this lesson's program only, as a warning.

Example: partial in real life​

In Module 9, EF Core scaffolding will generate entity classes from SchoolDb, such as a Student.cs with one property per column. You add rules and methods in a second file, like Student.Behaviour.cs here.

When a column is added to the database, you regenerate. The tool overwrites its own file and never touches yours. Without partial, every regeneration would erase your code.

Example: a preview of extension methods​

Static classes have one more job you will meet in Lesson 45. An extension method is a static method that can be called as if it belonged to another type, such as student.ToDto(), even though Student itself has no such method.

It is written in a static class, with this before the first parameter. LINQ in Module 8 is built entirely from extension methods, which is why marks.Max() works on an array.

Common mistakes​

  1. Static fields for user data. Shared by every request, lost on restart. Fix: keep such data in the database, or in objects created per request.
  2. Making a class static because it is "easier". Static code cannot be replaced in tests or through an interface. Fix: keep static for pure rules; use objects and interfaces for anything with dependencies, such as sending messages.
  3. const for values that might change. Other projects bake the value in when they compile. Fix: static readonly or configuration for values like fees and limits.
  4. Sealing an abstract class. It can then never be used. Fix: seal only concrete classes.
  5. Editing a generated partial file. The next regeneration deletes the change. Fix: put your code in your own partial file.

In real projects​

From 16 years of .NET projects

The duplicate receipt numbers in the hook are a real story. An admission system I reviewed kept "the next admission number" in a static field of a C# class.

It worked in testing with one user. On the first day of admissions, two clerks got the same number within an hour, and the API had restarted twice, starting from 1 each time.

We moved the counter into a database sequence that afternoon. Since then, any static field that is not readonly gets a hard look in my code reviews.

Best practices​

  • Use static classes for pure rules and helpers with no stored state.
  • Never keep user or business data in static fields; let the database own shared counters.
  • Use const for true constants, and static readonly for run-time values.
  • Seal concrete classes that are not designed for inheritance.
  • Keep generated code and your own code in separate partial files.

Recap​

  • Static members belong to the class; instance members belong to each object.
  • A static class holds only static members and can never be instantiated.
  • const is fixed at compile time; static readonly is fixed once at run time.
  • A static field is shared by every user of the app and resets on restart.
  • sealed stops inheritance; partial splits one class across files.
  • SchoolRules now lives in SchoolApp.Core.Rules, ready for the Web API.

Homework​

Library module task: Move the library rules into a static class LibraryRules.

  1. Add three constants: a loan period of 14 days, a fine of 2 per day, and a longest loan of 30 days.
  2. Add two methods: GetDueDate and CalculateFine.
  3. Make Book a sealed class.

A correct result looks like:

Output
Malgudi Days: issued 2026-07-10, due 2026-07-24
Returned 2026-07-28: fine 8
Loan 14 days, fine 2 per day, longest loan 30 days
Show the solution
library-assignment/042-library-static-sealed.cs
// Library homework, Lesson 42: a static class for library rules, and a sealed Book.
// Run with: dotnet run library-assignment/042-library-static-sealed.cs
Book book = new Book("Malgudi Days");
DateOnly issuedOn = new DateOnly(2026, 7, 10);
DateOnly dueOn = LibraryRules.GetDueDate(issuedOn);

Console.WriteLine($"{book.Title}: issued {issuedOn:yyyy-MM-dd}, due {dueOn:yyyy-MM-dd}");
Console.WriteLine($"Returned 2026-07-28: fine {LibraryRules.CalculateFine(dueOn, new DateOnly(2026, 7, 28))}");
Console.WriteLine($"Loan {LibraryRules.LoanDays} days, fine {LibraryRules.FinePerDay} per day, longest loan {LibraryRules.MaxLoanDays} days");

static class LibraryRules
{
public const int LoanDays = 14;
public const decimal FinePerDay = 2m;
public const int MaxLoanDays = 30; // the same limit as CK_BookIssues_MaxLoan (Lesson 13)

public static DateOnly GetDueDate(DateOnly issuedOn) => issuedOn.AddDays(LoanDays);

public static decimal CalculateFine(DateOnly dueOn, DateOnly returnedOn)
{
int daysLate = returnedOn.DayNumber - dueOn.DayNumber;
return daysLate > 0 ? daysLate * FinePerDay : 0m;
}
}

sealed class Book
{
public string Title { get; }

public Book(string title)
{
Title = title;
}
}

MaxLoanDays matches the CK_BookIssues_MaxLoan constraint from the Lesson 13 homework, so C# and SQL agree on the same limit.

Quiz​

  1. How do you call a static method?

    1. On an object, such as diya.GetGrade()
    2. On the class name, such as SchoolRules.GetGrade()
    3. Only from Main
    4. With new
    Show answer

    Answer: B. On the class name, such as SchoolRules.GetGrade() Static members belong to the class, not to an object.

  2. Why is a static counter field dangerous in a Web API?

    1. Static fields are slow
    2. Every request shares it, and it resets when the app restarts
    3. Web APIs cannot use static
    4. It uses too much memory
    Show answer

    Answer: B. Every request shares it, and it resets when the app restarts One field for all users, lost on restart: duplicates follow.

  3. Which works for a DateOnly value that never changes?

    1. const
    2. static readonly
    3. Both
    4. Neither
    Show answer

    Answer: B. static readonly const allows only compile-time values such as numbers and strings.

  4. What does sealed on a class do?

    1. Makes it static
    2. Prevents other classes from inheriting it
    3. Hides it
    4. Makes its fields read-only
    Show answer

    Answer: B. Prevents other classes from inheriting it Deriving from a sealed class fails with CS0509.

  5. Why do code generators use partial classes?

    1. They run faster
    2. Your code in another file survives regeneration
    3. They are required by EF Core
    4. To hide code
    Show answer

    Answer: B. Your code in another file survives regeneration The tool rewrites only its own file.

Interview questions​

Questions are grouped by experience level. Click a question to see a model answer. All questions: OOP interview questions.

Fresher (0-2 yrs)​

F What is the difference between a static member and an instance member? concept

A static member belongs to the class and has one shared copy, called on the class name, such as SchoolRules.GetGrade(92m). An instance member belongs to each object and is called on an object, such as diya.GetAgeOn(date).

See the example · Permalink

F What is a static class? concept

A class that holds only static members and can never be instantiated (error CS0712). It suits pure helpers and rules with no stored state, such as SchoolRules or Math.

See the example · Permalink

F What does sealed mean on a class? concept

No other class may inherit from it; trying fails with error CS0509. Seal concrete classes that were not designed for extension, such as Student and Teacher.

See the example · Permalink

Mid (3-5 yrs)​

M What is the difference between const and static readonly? concept

const is fixed at compile time and allowed only for simple types such as numbers and strings; other projects copy the value when they compile. static readonly is set once at run time and works for any type, such as DateOnly, and callers always read the current value.

See the example · Permalink

M What are partial classes used for? concept

Splitting one class across several files that the compiler joins. The main use is generated code: a tool such as EF Core scaffolding writes one file, and your own code in another file survives regeneration.

See the example · Permalink

M When should you avoid making a class static? concept

When it has dependencies or behavior you may want to replace, such as sending messages or reading the database. Static code cannot be swapped through an interface or faked in tests. Keep static for pure, deterministic rules.

See the example · Permalink

Senior (6-9 yrs)​

S Two clerks received the same admission number on the first day of admissions. The number came from a static field. Explain and fix. scenario

A static field is shared by every request in the app and resets when the app restarts; with several copies of the API, each has its own counter. Concurrent requests and restarts produce duplicates. Move the counter to the database, such as a SQL SEQUENCE, protected by a unique constraint.

How to answer: Explain the three failure modes (concurrency, restarts, multiple instances), the fix, and the safety net of a unique constraint. Mention reviewing every non-readonly static field.

See the example · Permalink

S Should classes be sealed by default? design

Many teams seal concrete classes by default and unseal deliberately. Sealing documents that a class was not designed for inheritance, prevents fragile overrides, and allows small performance gains. Leave classes open only when extension is part of the design.

How to answer: Present both views: the safety of sealing versus flexibility for library users. Mention that removing sealed later is safe, while adding it later breaks existing subclasses.

See the example · Permalink

Architect / Lead (10+ yrs)​

A Where should shared, changing state live in a system that runs several copies of a web API? design

Never in static fields or memory of one instance. Durable shared state belongs in the database (sequences, rows with constraints) or a shared store such as a distributed cache; per-request state lives in objects created for that request. Static members hold only constants and pure logic.

How to answer: Discuss scale-out, restarts, and concurrency, then the right home for each kind of state. Mention that this is why ASP.NET Core services are registered with scoped or transient lifetimes.

See the example · Permalink

Resources​

  • Code for this lesson: git tag lesson-42-static-sealed-partial (code repository coming soon)
  • Files: src/SchoolApp.Core/Rules/SchoolRules.cs, src/SchoolApp.Core/Entities/Student.cs, src/SchoolApp.Core/Entities/Student.Behaviour.cs, src/SchoolApp.Console/Program.cs, library-assignment/042-library-static-sealed.cs
  • Video: coming soon

What's next​

Statuses such as "Paid" and "Present" are still plain strings, and one typo breaks a rule. Next: enums for fixed lists of values, structs for small values, and records for data that compares by value. Lesson 43: Enums, structs, and records

Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.