Hello. This first module is a compact refresh of the C# and ASP.NET Core knowledge that frequently appears in senior .NET interviews. We will focus on predicting code behavior and explaining why, rather than memorizing definitions.
This lesson covers a cluster of concepts interviewers often combine in one short snippet: value versus reference types, default parameter passing, boxing, nullable values, and equality. By the end, you should be able to give a clear answer in the pattern: classify the type, identify what is copied, distinguish mutation from reassignment, then predict the result.
Priority: Must know. Plan for about 40 minutes, including the short resource blocks and answering the interview drills aloud before checking the model answers.
A reliable method for predicting C# snippets
Before looking at outputs, use this four-part trace:
- Classify each type. Is it a value type (
int,DateTime,struct, enum, nullable value type) or a reference type (class, array, string, delegate, interface)? - Identify the operation. Is it assignment, default parameter passing,
refpassing, boxing, or an equality comparison? - Ask what was copied. A value itself? A reference to an object? Or a reference to the caller’s variable through
ref? - Separate mutation from reassignment. Changing an existing object’s property is not the same as making a local variable refer to a different object.
One caution for interviews: avoid saying simply, “value types are on the stack and reference types are on the heap.” It is a useful beginner-level picture, but it is not a dependable rule for modern .NET. A value type can be inside a heap-allocated object or array; a reference can be held in a stack frame, a closure, or an async state machine. The language-level distinction is more important:
- A value-type variable contains its value directly.
- A reference-type variable contains a reference to an object.
That distinction predicts the behavior of assignments and parameters.
Interview drill 1: predict before reading on
Say the output aloud and explain each line.
public struct Discount
{
public decimal Percent { get; set; }
}
public class Campaign
{
public string Status { get; set; } = "Draft";
}
static void Update(Discount discount, Campaign campaign)
{
discount.Percent = 20;
campaign.Status = "Published";
campaign = new Campaign { Status = "Archived" };
}
var originalDiscount = new Discount { Percent = 10 };
var copiedDiscount = originalDiscount;
copiedDiscount.Percent = 15;
var firstCampaign = new Campaign();
var secondCampaign = firstCampaign;
secondCampaign.Status = "Review";
Update(originalDiscount, firstCampaign);
Console.WriteLine(originalDiscount.Percent);
Console.WriteLine(copiedDiscount.Percent);
Console.WriteLine(firstCampaign.Status);
Commit to an answer before continuing.
Assignment and default parameter passing
The output is:
10
15
Published
Here is the reasoning.
Discount is a struct, therefore a value type. This assignment:
var copiedDiscount = originalDiscount;
copies the value. originalDiscount and copiedDiscount begin with the same data, but they are independent variables. Updating copiedDiscount.Percent does not affect originalDiscount.Percent.
Campaign is a class, therefore a reference type. This assignment:
var secondCampaign = firstCampaign;
copies the reference, not the object. Both variables identify the same Campaign object. Updating secondCampaign.Status mutates that shared object, so firstCampaign.Status becomes "Review" too.
Now consider the method call:
Update(originalDiscount, firstCampaign);
C# passes parameters by value by default, but “by value” means different things depending on what the variable contains:
| Argument type | What the method receives by default | Visible to caller? |
|---|---|---|
Value type, such as Discount | A copy of the value | Mutating the parameter affects only the copy |
Reference type, such as Campaign | A copy of the reference | Mutating the object is visible; reassigning the parameter is not |
Inside Update, this line changes only the local struct copy:
discount.Percent = 20;
So originalDiscount.Percent remains 10.
This line mutates the single shared Campaign object:
campaign.Status = "Published";
So the caller sees "Published".
Finally, this line changes only the method’s local copy of the reference:
campaign = new Campaign { Status = "Archived" };
The caller’s firstCampaign variable still points to the originally created object, whose status is "Published".
A concise senior-level answer is:
C# passes arguments by value unless a modifier such as
reforoutis used. With a class, the copied value is a reference, so the method can mutate the same object but cannot replace the caller’s reference. With a struct, the copied value is the struct data itself.
Method parameters and modifiers - C# reference
Read Microsoft’s C# reference to reinforce the distinction between modifying an object and reassigning a parameter. This is one of the most common sources of inaccurate interview answers.
In the opening section, read from the default-passing explanation. Focus on the statement that classes are still passed by value: it is the reference that is copied. Then read the section “Pass by value and pass by reference,” especially the subsection beginning “The preceding examples modified properties of a parameter.” Read the reassignment comparison and its ref example. Notice that reassignment is invisible to the caller unless the parameter itself is passed by reference.
When ref and out change the rules
Use ref when the method truly needs to work with the caller’s variable itself, including replacing its value.
static void ReplaceCampaign(ref Campaign campaign)
{
campaign = new Campaign { Status = "Archived" };
}
var campaign = new Campaign { Status = "Draft" };
ReplaceCampaign(ref campaign);
Console.WriteLine(campaign.Status); // Archived
Here, campaign in the method is an alias for the caller’s variable. The replacement is visible to the caller.
The practical distinctions are:
- No modifier: default choice. The method receives a copy of the argument’s value.
ref: caller initializes the variable; method can read it, mutate it, or replace it.out: caller need not initialize the variable; method must assign it before returning.in: read-only passing intended mainly to avoid copying a large struct in a measured performance-sensitive path.
Do not recommend ref merely to make class-object mutations visible; they are already visible under ordinary parameter passing. Introducing ref makes aliasing and ownership less clear, and should have a specific reason.
Useful nuance: copying a struct is not necessarily a deep copy. If a struct contains a field that refers to a mutable class object, copying the struct copies that reference. Both struct copies can still reach and mutate that nested object.
Boxing: a value type temporarily treated as an object
Boxing happens when a value type must be represented as object or as an interface it implements.
int count = 42;
object boxedCount = count; // Boxing
count = 99;
int restored = (int)boxedCount; // Unboxing
Console.WriteLine(restored); // 42
Boxing creates an object that contains a copy of the value. Changing count later has no effect on the boxed value. Unboxing requires an explicit cast and copies the value out again.

A classic interview follow-up is this:
object boxed = 42;
long value = (long)boxed;
This throws InvalidCastException. The runtime object contains a boxed int, not a boxed long. Although an int can normally be converted to long, unboxing requires the boxed runtime type to match the requested value type. Unbox first, then convert if necessary:
long value = (long)(int)boxed;
Boxing is usually not a production issue in isolated code, but it matters in hot paths because it creates extra allocations and garbage-collection work. Common historical sources include non-generic collections such as ArrayList, APIs accepting object, and interface-based code involving structs.
Prefer generic collections:
var ids = new List<int> { 10, 20, 30 };
A List<int> stores int values without boxing each element, unlike a non-generic collection that stores values as object.
What is Boxing in C# and how it affects memory and speed
Watch “What is Boxing in C# and how it affects memory and speed” by Nick Chapsas for a concrete view of the boxing allocation and the explicit unboxing cast.
Watch boxing in action to see an int assigned to object and the resulting heap allocation. Then watch unboxing rules, focusing on why extracting the value requires an explicit cast. Treat the memory diagram as a useful conceptual model rather than an absolute rule about where every local exists at runtime.
Nullability: runtime values and compiler assistance
There are two related but different ideas:
- Null at runtime: a reference can contain no object reference.
- Nullable reference types: compiler annotations and warnings that help identify unsafe null handling before runtime.
#nullable enable
string? campaignName = null;
// Console.WriteLine(campaignName.Length);
// Compiler warning; would throw NullReferenceException at runtime.
int? length = campaignName?.Length;
string displayName = campaignName ?? "Untitled";
string? means the compiler should treat null as an expected possibility. It does not prevent a null value at runtime. The null-conditional operator, ?., avoids dereferencing null; here it produces an int?. The null-coalescing operator, ??, supplies a fallback.
At application boundaries, validate required input rather than relying on the nullability annotation alone:
public CampaignService(ICampaignRepository repository)
{
ArgumentNullException.ThrowIfNull(repository);
_repository = repository;
}
The null-forgiving operator, !, only suppresses the compiler warning:
string name = campaignName!;
It performs no runtime check. In interview code, using ! should prompt the question: what invariant proves this cannot be null? If there is no solid answer, prefer a guard, conditional handling, or a more accurate type.
Nullable value types are different:
int? optionalId = null;
int? is shorthand for Nullable<int>, a value type that can represent either an int or “no value.” A useful edge case: boxing a nullable value with no value produces null; boxing one with a value boxes the underlying value type.
int? missing = null;
int? present = 7;
object? first = missing; // null
object? second = present; // boxed int
Equality: identity versus meaningful data
Equality questions are difficult because ==, Equals, and ReferenceEquals are related but not interchangeable.
- Reference equality / identity: are these variables referring to the exact same object?
- Value equality: do these instances contain data that should count as equal?
A plain class uses reference equality by default:
public class Campaign
{
public int Id { get; init; }
public string Name { get; init; } = "";
}
var first = new Campaign { Id = 7, Name = "Spring" };
var second = new Campaign { Id = 7, Name = "Spring" };
var alias = first;
Console.WriteLine(first == second); // False
Console.WriteLine(first.Equals(second)); // False
Console.WriteLine(ReferenceEquals(first, second)); // False
Console.WriteLine(first == alias); // True
first and second have matching data but are distinct objects. alias holds a copied reference to the same object as first.
A record class is still a reference type, but it is designed for data-oriented value equality:
public record CampaignKey(int AccountId, string Code);
var first = new CampaignKey(10, "SPRING");
var second = new CampaignKey(10, "SPRING");
Console.WriteLine(first == second); // True
Console.WriteLine(first.Equals(second)); // True
Console.WriteLine(ReferenceEquals(first, second)); // False
The two record instances are distinct objects, yet their generated equality considers their data equal.
Read Microsoft’s equality comparison guide to build an interview-safe explanation of identity, value equality, strings, records, and hash-based collections.
In the opening section “Value types, reference types, and equality defaults,” start at the distinction between equality kinds. Continue through the plain class, plain struct, and tuple examples. Next read “Types can define different equality semantics” and “Use records for value equality.” Pay particular attention to the exceptions: string is a reference type but compares textual content, while records generate coordinated Equals, GetHashCode, ==, and != implementations.
The equality contract matters in real applications
If two objects are equal according to Equals, they must produce the same hash code. Otherwise, Dictionary<TKey, TValue> and HashSet<T> can behave incorrectly.
For a data-oriented class that cannot be a record, implement equality consistently:
- Override
Equals(object?). - Override
GetHashCode()using the same fields. - Usually implement
IEquatable<T>. - If you overload
==, also overload!=and keep both consistent withEquals.
Do not use mutable fields that participate in equality as keys in a Dictionary or members of a HashSet. If a key’s hash-relevant fields change after insertion, it may no longer be found in the expected bucket.
For domain design, choose equality semantics deliberately:
- A
Money,DateRange, or immutable request DTO is usually data-like and may suit value equality. - A tracked EF Core entity often has identity semantics. Two separate
Campaigninstances with the same display name are not necessarily the same business entity. - A stable identifier such as
CampaignIdcan define business identity, but decide whether that is appropriate for every lifecycle state, including unsaved entities.
For a null check that should not be affected by an overloaded equality operator, use pattern matching:
if (campaign is null)
{
return;
}
ReferenceEquals(a, b) is useful when you specifically need to test identity, including when records or custom classes have value-based equality. It is not generally useful for value types, since supplying value types to it involves boxing.
Interview drill 2: give a senior-level explanation
Answer aloud in about 90 seconds:
A teammate says, “Classes are passed by reference, so I will use
refwhenever I pass a service request object to avoid copying it.” How would you correct the statement, and what concerns would you raise about the design?
Model answer
The statement is imprecise. C# passes method arguments by value by default. When the argument is a class instance, the copied value is the reference. That means the callee can mutate the shared object, but it cannot make the caller’s variable point to a different object.
ref is not needed to avoid copying a reference; the reference is already small and copied by value. Adding ref permits the method to replace the caller’s variable and makes that behavior part of the API contract. I would use it only when replacement is intentional and improves the API’s clarity.
For request objects, I would also question mutation. In an ASP.NET Core application, mutable request models passed across layers can make validation, logging, retries, and concurrent behavior harder to reason about. Prefer clear ownership: validate at the boundary, map into a domain command or immutable data model where appropriate, and return an explicit result rather than silently replacing or mutating input.
Key takeaways
- Value-type assignment and default parameter passing copy the value; class assignment and default parameter passing copy the reference.
- A copied class reference can mutate the same object, but reassigning a parameter affects only the method’s local reference unless
refis used. - Boxing wraps a value type as an object or interface value; unboxing needs an explicit cast to the exact boxed type and can add allocation cost.
- Nullable reference annotations help the compiler, but runtime validation and deliberate null handling remain necessary.
- Equality must be chosen intentionally: plain classes default to identity, while strings and records provide value-oriented semantics. Equal objects must have equal hash codes.
Next, we will move from predicting behavior to critiquing a small C# object model: encapsulation, interfaces, polymorphism, composition over inappropriate inheritance, and dependency inversion.
Can't find a good explanation? Sign up and we'll make it for you
Sign up