static, sealed and partial: Three Class Modifiers – C# | Lesson 42
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:
staticdecides whether you need an object at all.sealeddecides whether anyone may inherit.partiallets 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
constandstatic 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 member | Static member | |
|---|---|---|
| Belongs to | One object | The class itself |
| Called as | diya.GetAgeOn(...) | SchoolRules.GetGrade(...) |
| How many copies | One per object | Exactly one, shared by everyone |
Can use this object's data | Yes | No: 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:
| Type | Static member | Instance member |
|---|---|---|
int | int.Parse("45500") | mark.ToString() |
string | string.Join(", ", names) | name.Trim() |
DateOnly | DateOnly.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?
const | static readonly | |
|---|---|---|
| Value fixed | When the code is compiled | Once, when the program first uses the class |
| Allowed types | Numbers, bool, char, string | Any type, such as DateOnly |
| SMS example | PassMark = 33 | YearStart = 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
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.
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;
}
}
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
// 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}";
}
}
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.
| Problem | Static counter in C# | ReceiptNumbers SEQUENCE in SchoolDb (Lesson 22) |
|---|---|---|
| Two requests at once | Can duplicate | Never duplicates |
| App restart | Starts again at 1 | Continues |
| Several API copies | One counter each | One 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
- 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.
- 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.
constfor values that might change. Other projects bake the value in when they compile. Fix:static readonlyor configuration for values like fees and limits.- Sealing an abstract class. It can then never be used. Fix: seal only concrete classes.
- Editing a generated partial file. The next regeneration deletes the change. Fix: put your code in your own partial file.
In real 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
constfor true constants, andstatic readonlyfor 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.
constis fixed at compile time;static readonlyis fixed once at run time.- A static field is shared by every user of the app and resets on restart.
sealedstops inheritance;partialsplits one class across files.SchoolRulesnow lives inSchoolApp.Core.Rules, ready for the Web API.
Homework
Library module task: Move the library rules into a static class LibraryRules.
- Add three constants: a loan period of 14 days, a fine of 2 per day, and a longest loan of 30 days.
- Add two methods:
GetDueDateandCalculateFine. - Make
Bookasealedclass.
A correct result looks like:
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 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
How do you call a static method?
Show answer
Answer: B. On the class name, such as SchoolRules.GetGrade() Static members belong to the class, not to an object.
Why is a static counter field dangerous in a Web API?
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.
Which works for a DateOnly value that never changes?
Show answer
Answer: B. static readonly const allows only compile-time values such as numbers and strings.
What does sealed on a class do?
Show answer
Answer: B. Prevents other classes from inheriting it Deriving from a sealed class fails with CS0509.
Why do code generators use partial classes?
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.