Prepare for C# interview questions grouped by experience level.
C# Interview Question & Answers
0-2 Years
C# is a modern, object-oriented programming language developed by Microsoft, originally built to run on the .NET Framework, Microsoft's own application platform for Windows. It's since expanded to run cross-platform through .NET Core and the current, unified .NET, no longer tied exclusively to Windows.
The .NET Framework was the original, Windows-only implementation. .NET Core was built as a cross-platform, open-source successor, running on Windows, Linux, and macOS alike. Modern .NET has since unified both lineages into a single, current platform, simply called .NET, continuing that cross-platform direction going forward.
The C# compiler compiles source code into Intermediate Language (IL), a genuinely platform-independent bytecode. At runtime, the Common Language Runtime (CLR) genuinely uses Just-In-Time compilation to actually translate that IL into native machine code the specific hardware can genuinely execute directly.
The CLR is the genuine execution engine for .NET applications, handling memory management, garbage collection, type safety, and exception handling, providing the actual managed environment C# code genuinely runs within, rather than executing directly against raw hardware.
A compile-time error is caught by the compiler before the program ever actually runs, like a missing semicolon or a genuine type mismatch. A runtime error only actually surfaces while the program is executing, like a null reference exception, which the compiler generally has no way to catch in advance.
static void Main() is the entry point every C# console application genuinely starts executing from. It's required because the runtime needs one single, consistent, known place to actually start running the program, and by convention, that specific place is always genuinely the Main method.
A value type, like int or a struct, genuinely stores its actual data directly in the variable itself, and copying it creates a genuinely independent copy. A reference type, like a class instance, genuinely stores a reference (a memory address) to where the actual data lives, so copying it copies genuinely just the reference, with both variables then pointing to the exact same underlying object.
int, double, float, decimal, bool, and char are all genuinely common built-in value types, along with struct, which lets you define your own genuinely custom value type. Each stores its actual data directly rather than through a genuine reference.
For a value type, both genuinely compare actual values. For a reference type, == genuinely, by default, compares whether two references point to the exact same object in memory, while .Equals() can genuinely be overridden by a class to define its own custom notion of equality, comparing actual content rather than just reference identity.
var lets the compiler genuinely infer a variable's type from the value assigned to it at compile time, while the variable still genuinely remains strongly typed underneath, exactly as if you'd written the type explicitly yourself. It's purely a genuine convenience for the developer, not a change in how the variable actually behaves at runtime.
int? age; declares a nullable int, letting it genuinely hold either an actual integer value or null, which a plain, non-nullable int can never genuinely hold on its own. It's genuinely useful for representing a value that might legitimately be missing or genuinely not yet known.
const genuinely must be assigned a value at compile time and can never change afterward, applying only to a genuinely built-in, primitive-like type. readonly can genuinely be assigned either at declaration or within a constructor, and its actual value can genuinely differ between different instances of the exact same class.
A class defines a genuine blueprint for an object's own data and behavior. Person p = new Person(); genuinely creates a new object, called an instance, of the Person class, using the new keyword to actually allocate and initialize it.
A constructor is a genuinely special method automatically called when a new object is actually created, typically used to initialize the object's own fields. It shares the exact same name as the class itself and genuinely has no explicit return type at all, not even void.
public members are accessible from genuinely anywhere. private members are accessible only from within the class itself. protected members are accessible from within the class and its genuine subclasses. internal members are accessible only from within the genuinely same assembly (project).
A class is a genuine reference type, allocated on the heap, and supports inheritance. A struct is a genuine value type, typically allocated on the stack, and doesn't support inheritance from another struct or class. Structs are genuinely used for small, simple data structures where value-type copying semantics are actually desired.
Method overloading lets you genuinely define multiple methods sharing the exact same name but differing in their actual parameter list, the number or types of parameters. The compiler determines which specific overloaded version to actually call based on the arguments genuinely provided at each individual call site.
this refers to the genuine current instance a method is operating on, commonly used to distinguish a parameter from a field sharing the exact same name, like this.name = name; inside a constructor, or to actually pass the current object as an argument to another method.
A for loop gives you genuine full manual control over the counter and the loop's exact condition. A while loop genuinely checks its condition before each iteration. A foreach loop genuinely iterates directly over each element of a collection, without needing to manage an index at all.
break genuinely exits the loop entirely, stopping all further iterations. continue genuinely skips just the rest of the current iteration and moves on to the next one, without exiting the loop as a whole.
A switch statement compares genuinely one single value against several possible fixed cases, generally reading more cleanly than a long if-else chain specifically when you're comparing the exact same variable against several different possible discrete values.
A params parameter, declared with params int[] numbers, lets a method accept a genuinely variable number of arguments of the same type, treated internally as an array. It solves the genuine problem of needing several genuinely separate overloaded methods just to accept a different number of arguments each time.
By default, C# passes arguments genuinely by value, so a change made inside a method has no effect on the original variable outside it. The ref keyword genuinely passes an argument by reference instead, so a change made inside the method genuinely does affect the original variable back in the calling code.
ref requires the variable to genuinely already be initialized before being passed in, and the method can genuinely both read and modify it. out doesn't genuinely require prior initialization, but the method itself must genuinely assign a value to it before actually returning.
int[] numbers = { 1, 2, 3, 4, 5 }; declares an array of 5 integers with those specific given values. Array elements are accessed using a genuinely zero-based index, so numbers[0] genuinely refers to the very first element.
List<T> is a genuinely resizable, generic collection that can dynamically grow or shrink as elements are actually added or removed. A plain array has a genuinely fixed size decided when it's first created, which is exactly why List<T> is generally preferred whenever the actual number of elements genuinely isn't known reliably in advance.
Both are genuinely type-safe in modern C#, since List<T> is a generic collection, and its type parameter T is genuinely specified explicitly, like List<int>, ensuring only that specific type can genuinely be added to it, checked at compile time rather than discovered as a runtime error.
A Dictionary stores genuine key-value pairs, letting you actually look up a value quickly by its associated key, rather than needing to search through a list sequentially. It's genuinely useful whenever data naturally maps one specific identifier to a specific, associated value.
list.Add(item) genuinely appends a new item to the end of the list. list.Remove(item) genuinely removes the first occurrence of that specific item from the list, if it's genuinely actually present at all.
IEnumerable<T> is a genuine interface representing something that can be iterated over, without guaranteeing any specific underlying implementation or genuine additional capability like indexing. List<T> is a genuinely concrete class implementing IEnumerable<T>, and additionally provides direct indexed access and methods like Add and Remove that a plain IEnumerable<T> genuinely doesn't guarantee.
A try block wraps code that might genuinely throw an exception. A catch block, matched by the actual exception's specific type, handles it if one is genuinely thrown. A finally block, if present, genuinely always runs afterward, whether or not an exception actually occurred.
Unlike Java, C# genuinely doesn't require a method to declare which exceptions it might throw, and there's genuinely no compiler-enforced distinction between a checked and an unchecked exception at all. Every exception in C# behaves genuinely the way an unchecked exception would in Java, with genuinely no compile-time requirement to actually handle it.
Catching a genuinely specific exception type, like FileNotFoundException, handles only that exact particular kind of error. Catching the base Exception class catches genuinely any exception at all, which is useful as a genuinely broad, final fallback, though it does lose the ability to actually handle each genuinely different, specific error type appropriately.
Code inside a finally block genuinely always runs after a try-catch, whether an exception was thrown and caught, or the try block genuinely completed successfully with no exception at all. It's the standard place for cleanup logic, like closing a file, that needs to genuinely happen no matter what.
Define a class extending Exception, then use throw new YourCustomException('message'); to actually raise it at the appropriate point in your code. A custom exception lets calling code genuinely catch a specific, named failure mode rather than a generic built-in exception type.
3-6 Years
The colon syntax, class Dog : Animal, lets Dog genuinely inherit fields and methods from Animal, gaining its own behavior automatically while genuinely being free to add its own additional members or override an inherited one to behave differently.
An abstract class can genuinely hold both abstract methods (with no implementation) and concrete methods (with a full implementation), and a class can only extend one abstract class. An interface traditionally held only method signatures with no implementation, though modern C# allows default implementations too, and a class can genuinely implement multiple interfaces at once.
Overriding lets a derived class genuinely provide its own implementation of a method already marked virtual in its base class, using the override keyword. Without marking the base method virtual, C# genuinely won't allow it to actually be overridden at all.
Overriding, using override, genuinely replaces the base method's behavior polymorphically, so calling it through a base class reference still genuinely invokes the derived version. Hiding, using new, genuinely defines a completely separate method that only actually gets called when accessed through a reference genuinely typed as the derived class itself.
Polymorphism lets a single method call genuinely behave differently depending on the actual object it's operating on. Calling an overridden method through a base class reference genuinely invokes whichever specific derived class's implementation the actual, real underlying object genuinely is.
A property, using get and set accessors, lets you genuinely control access to a value, adding validation or genuinely additional logic when it's read or written, unlike a plain public field, which genuinely allows direct, unrestricted access with no way to intercept that access at all.
public string Name { get; set; } is an auto-implemented property, where the compiler genuinely generates a hidden, private backing field automatically, saving you from needing to genuinely write that boilerplate field and its accessors manually when no genuinely custom logic is actually needed.
A delegate is a genuine type representing a reference to a method, letting you actually pass a method around as a value, store it in a variable, or invoke it indirectly. It solves the genuine problem of needing to genuinely parameterize behavior itself, beyond just data, similar in spirit to a function pointer in another language.
An event is genuinely built on top of a delegate, providing a controlled way for a class to genuinely notify subscribers when something happens, while restricting external code from directly invoking it or genuinely replacing the entire list of subscribers, something a plain public delegate field genuinely wouldn't restrict.
A lambda expression, like x => x * 2, provides a genuinely compact, inline way to define a small, anonymous function, replacing what used to require a genuinely more verbose named method or an anonymous delegate written out in full.
Func<T, TResult> represents a method that takes an argument of type T and returns a value of type TResult. Action<T> represents a method that takes an argument of type T but genuinely returns nothing at all. Both are built-in generic delegate types, saving you from needing to define a genuinely custom delegate type for every common method signature shape.
LINQ (Language Integrated Query) lets you genuinely write query-like expressions directly in C#, to actually filter, sort, or transform data from a collection, a database, or XML, using a genuinely consistent, unified syntax rather than a different specific API for each different kind of data source.
numbers.Where(n => n > 10) genuinely returns every element in the numbers collection satisfying the given condition, using a lambda expression to actually define that specific filtering logic.
Method syntax chains methods together, like numbers.Where(...).Select(...). Query syntax uses a genuinely SQL-like structure instead, like from n in numbers where n > 10 select n. Both genuinely compile down to the exact same underlying code, and the choice between them is purely a genuine matter of readability preference.
Select() genuinely transforms each element in a collection according to a given expression, returning a genuinely new collection of the transformed results, conceptually similar to map() in many other languages.
First() genuinely returns the first matching element, and throws an exception if genuinely nothing actually matches. FirstOrDefault() genuinely returns the first matching element, or the type's own default value (like null for a reference type, or 0 for an int) if genuinely nothing matches at all, avoiding an exception.
A LINQ query defined with Where() or Select() genuinely doesn't actually execute at the moment it's written. It only actually runs when the results are genuinely enumerated, like with a foreach loop or a call to ToList(). This matters because the underlying data source could genuinely change between when the query is defined and when it's actually executed, affecting the actual results returned.
A generic collection, like List<T>, is genuinely type-safe, catching a mismatched type at compile time rather than at runtime, and it also genuinely avoids the performance overhead of boxing a value type, which a non-generic collection like the older ArrayList would genuinely require.
Boxing genuinely wraps a value type in an object reference, moving it onto the heap. Unboxing genuinely reverses that. Both carry real, genuine performance overhead, which is exactly why a genuinely non-generic collection storing a value type as a plain object, requiring constant boxing and unboxing, is generally avoided in modern C# code.
IEnumerable<T> genuinely executes its operations in memory, on the client side, once the data is actually already retrieved. IQueryable<T> genuinely builds up an expression tree that can be translated and executed at the actual data source itself, like a database, letting filtering and sorting happen genuinely more efficiently before the data is actually retrieved at all.
A HashSet<T> genuinely enforces uniqueness, automatically rejecting an attempt to add a value that's already present, and offers genuinely faster membership checks than a List<T>'s linear search. It's the genuinely more natural, purpose-built choice whenever a collection genuinely needs to guarantee no duplicate values.
async and await let you genuinely write asynchronous code that reads almost like ordinary, synchronous, top-to-bottom code, without needing genuinely deeply nested callbacks. They solve the genuine problem of a slow operation, like a network call, blocking the calling thread while it waits.
A synchronous call genuinely blocks the calling thread until the operation actually completes. An asynchronous call, using await, genuinely lets the calling thread continue doing other work while the operation runs in the background, resuming the original method once that operation actually finishes.
A Task represents an genuinely ongoing or completed asynchronous operation, similar in spirit to a Promise in JavaScript, letting you actually await its completion or check its genuine current status, whether it's still running, completed successfully, or genuinely faulted with an error.
Calling .Result or .Wait() genuinely blocks the calling thread synchronously, defeating the entire genuine purpose of asynchronous programming, and it can also genuinely cause a deadlock in certain contexts, like a UI application's own main thread, where the awaited task itself needs that exact same blocked thread to actually complete.
6-8 Years
Generics let you write a genuinely single method or class that works correctly across multiple different data types, specified as a type parameter, like List<T>, without needing to actually write a genuinely separate, near-identical version for each individual type by hand.
An extension method lets you genuinely add a new method to an existing type, including one you don't own the source code for, without actually modifying that original type directly. It's implemented as a genuinely static method in a static class, with the this keyword before its first parameter.
Enabling nullable reference types lets the compiler genuinely warn you at compile time when a reference type variable might genuinely be null but is being used in a context that doesn't actually handle that possibility, catching a genuinely common source of a NullReferenceException much earlier than it would otherwise actually surface at runtime.
Pattern matching lets you genuinely check a value's type or shape directly within a conditional expression, like if (obj is string s), which both genuinely checks whether obj is actually a string and, if so, assigns it directly to the new variable s in that exact same expression.
A record is genuinely designed for immutable data, automatically providing value-based equality (comparing actual content rather than reference identity) and a genuinely useful default ToString() implementation, which a regular class would otherwise require writing manually yourself.
A shallow copy, created with MemberwiseClone(), duplicates only the top-level fields, so a nested reference type field is still shared by reference between the original and the copy. A deep copy duplicates every nested level too, typically requiring you to actually implement that logic yourself, since C# genuinely provides no single, built-in, universal way to deep copy an arbitrary object.
The garbage collector periodically genuinely scans the managed heap for objects that are genuinely no longer reachable, meaning nothing in the program can still actually reference them, and reclaims their memory automatically, without a developer needing to manually free memory the way an unmanaged language would genuinely require.
IDisposable defines a Dispose() method for genuinely releasing an unmanaged resource, like a file handle or a database connection, that the garbage collector doesn't genuinely know how to clean up on its own, since garbage collection only genuinely manages memory, not other kinds of external resources.
using (var connection = new SqlConnection(...)) { ... } genuinely guarantees Dispose() is actually called on the object once the block finishes, whether it completes normally or an exception is genuinely thrown, ensuring a resource implementing IDisposable is genuinely, reliably cleaned up without requiring an explicit try-finally block written by hand.
Generation 0 holds newly created objects, most of which genuinely die quickly and get collected fast. Objects surviving several collections get promoted to Generation 1, then eventually Generation 2. This structure exists because most objects genuinely are short-lived, so optimizing heavily for that common case improves overall garbage collection performance.
Even with automatic garbage collection, an object can't be reclaimed as long as something genuinely still holds a reference to it. A memory leak in C# usually means something, an event handler that was never unsubscribed, a growing static collection with no limit, is genuinely preventing garbage collection from ever actually reclaiming that memory.
8-10 Years
The TPL provides a genuinely higher-level API for actually writing parallel and concurrent code, abstracting away much of the genuinely low-level thread management a developer would otherwise need to handle manually, letting you express concurrent work in terms of Tasks rather than genuinely raw, individual threads.
Task.Run() genuinely schedules work onto the thread pool, which reuses existing threads efficiently rather than creating a genuinely brand new, dedicated one for every single piece of work. Creating a new Thread directly genuinely allocates a dedicated, actual thread, which carries more real overhead and is generally reserved for a genuinely specific, long-running task needing its own dedicated thread.
A race condition happens when the genuine outcome of concurrent code depends on the unpredictable timing of multiple threads accessing genuinely shared, mutable data. The lock keyword genuinely ensures only one thread at a time can execute a specific critical section of code, preventing that genuinely unsafe, concurrent access to shared state.
lock is genuinely syntactic sugar for a try-finally block using Monitor.Enter() and Monitor.Exit(), guaranteeing the lock is genuinely released even if an exception occurs. Using Monitor directly gives you genuinely more explicit control, like a timeout on acquiring the lock, that the simpler lock keyword doesn't directly expose.
A deadlock happens when two threads are each genuinely waiting on a resource the other one currently holds, so neither thread can ever actually proceed. A thread dump, or a debugger's own thread view, shows exactly which threads are genuinely blocked and what they're waiting on, which usually reveals the specific circular dependency causing it.
SemaphoreSlim controls access to a resource by allowing a specified number of threads through at once, rather than a lock's strict one-at-a-time restriction. It also genuinely supports asynchronous waiting through WaitAsync(), letting you limit concurrent access to a resource in async code without blocking a thread while waiting, something a plain lock genuinely can't do.
ConfigureAwait(false) tells an awaited Task not to genuinely try resuming on the exact original synchronization context afterward. It's genuinely used in library code that doesn't need to interact with a UI thread specifically, avoiding unnecessary genuine overhead and reducing the real risk of a deadlock in certain specific contexts.
An exception thrown inside a genuinely fire-and-forget async call (one whose returned Task is never actually awaited or observed) can genuinely be lost silently, or in some .NET versions, crash the entire process. Wrapping such calls with proper genuine error handling, or explicitly observing the returned Task, is important to actually avoid that specific silent failure.
Dependency injection provides a class with the genuine dependencies it actually needs from an external source, rather than the class itself genuinely creating them directly. .NET's built-in DI container lets you genuinely register a service once, and it's then automatically injected into any constructor that genuinely declares a dependency on that same service's interface.
Singleton creates genuinely one single instance shared across the entire application's lifetime. Scoped creates genuinely one instance per request (in a typical web application). Transient creates a genuinely brand new instance every single time it's actually requested.
A Repository wraps all database access behind a genuine interface, so the rest of the application talks to that interface instead of directly to an ORM like Entity Framework. It solves the genuine problem of making it easier to genuinely swap the underlying data access technology later, and makes unit testing business logic possible without touching a genuinely real database at all.
Singleton ensures a class has genuinely exactly one instance shared everywhere it's actually used. A thread-safe implementation typically uses a genuinely static, readonly field initialized directly (leveraging the CLR's own thread-safe static initialization guarantee), avoiding the genuinely more manual double-checked locking pattern that was historically needed.
I'd favor implementing an interface, and composition more generally, when the new functionality genuinely doesn't share a strong is-a relationship with an existing class, since composition tends to be more flexible and easier to genuinely change later than a deep inheritance hierarchy would be.
Organize the application into genuinely well-defined layers or feature areas, keeping business logic genuinely separate from data access and presentation concerns, rather than one single, enormous project mixing every genuine concern together indiscriminately.
10+ Years
I'd weigh the genuine benefits of the newer version, performance improvements, new language features a team would genuinely find useful, against the real cost of introducing a genuinely different .NET version alongside an organization's existing, established tooling and deployment conventions.
Run it incrementally, using Microsoft's own official upgrade tooling wherever genuinely possible, leaning on existing test coverage to catch a genuine regression early, and migrating one genuinely bounded project or module at a time rather than attempting one single, large, disruptive migration all at once.
I check whether it's genuinely solving the real problem or just its symptom, whether dependency injection and separation of concerns are genuinely applied sensibly, and whether it's genuinely consistent with patterns already established elsewhere in the codebase, rather than introducing a genuinely inconsistent, one-off approach.
Automate what can genuinely be automated, a linter or analyzer like StyleCop or Roslyn analyzers enforced directly in CI, so standards aren't purely a matter of individual opinion during manual code review. For architectural conventions that genuinely resist full automation, I'd document the handful of decisions that actually matter most.
I'd weigh the genuine, real benefit, catching a genuine class of bug earlier, against the real, upfront cost of actually enabling it across an existing codebase, which typically surfaces a genuinely large number of warnings needing to actually be addressed. I'd pilot it on a genuinely smaller, newer part of the codebase first.
I'd check exception logging and any available stack trace details from production first, since a genuinely intermittent null reference often traces back to a specific race condition or an edge case in data that only genuinely shows up under real, actual production traffic and data patterns.
Track application error rate, response latency, and memory usage over time, alerting on meaningful deviation from a normal, established baseline. A genuinely, gradually growing memory usage trend is often an early warning sign of a memory leak well before it actually causes a full, hard crash.
Treat the library's actual public API as a genuine contract with every consuming team. Adding something new is generally safe. Changing or removing an existing public method's signature needs a documented deprecation period, using the Obsolete attribute with a clear message, before actual removal.
I'd check the deployment's own genuine change log first, since a recent deployment is the most likely genuine suspect, and roll back immediately if the issue is genuinely severe, prioritizing stopping active user impact over fully understanding root cause immediately.
I'd monitor genuine memory and performance trends proactively in production, and address a slowly growing memory usage pattern or a genuinely degrading response time before it actually starts visibly, noticeably affecting real users, rather than waiting for a genuine complaint to surface the problem first.
This is a judgment question interviewers use to see how you reason under genuine uncertainty, not to test a specific textbook fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now.
I'd walk through an actual deadlock scenario together, showing concretely how blocking synchronously on an async call can genuinely lock up a UI thread, rather than explaining the risk purely in the abstract. Seeing a genuinely real deadlock reproduce firsthand tends to shift that habit far more effectively than a general warning alone.
I wouldn't lead with modernization as an abstract goal. I'd point to a specific, real, concrete benefit, cross-platform deployment capability, a genuine performance improvement, that would actually matter for their own specific upcoming project, and let that concrete benefit make the case rather than arguing for the newer platform in the abstract.
I'd frame it around whether the logic genuinely needs to be reused from more than one place, like a background job or a genuinely different entry point, in which case a separate service class is the clearer fit. Grounding the discussion in the specific, concrete logic at hand resolves it faster than a general, abstract architectural preference.
I'd translate the debt into terms leadership already tracks: a specific incident or delayed feature that traced directly back to it, and how much longer a typical change in that specific area now takes compared to a genuinely well-structured part of the same codebase. Framed as a velocity problem with a real, already-incurred cost, it competes far better for prioritization than framed as a general code-quality concern.




