C# Senior Engineer Interview Questions and Answers
What is the difference between interface and abstract class in C#?
Interfaces define a contract that a class must implement, specifying what methods and properties a class should have, but not how they should be implemented. Abstract classes, on the other hand, can provide a partial implementation of methods and properties, as well as abstract members that must be implemented by derived classes.
What are the SOLID principles?
SOLID is a set of five design principles that aim to make software designs more understandable, flexible, and maintainable. They are:
- Single Responsibility Principle (SRP): A class should have only one reason to change.
- Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
- Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering the correctness of the program.
- Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Explain the difference between async and await keywords in C#.
The async keyword is used to mark a method as asynchronous, allowing it to execute without blocking the calling thread. The await keyword is used within an async method to pause the execution of the method until an asynchronous operation completes, without blocking the thread.
What is LINQ?
LINQ (Language Integrated Query) is a powerful feature in C# that allows you to write queries against collections of objects, XML documents, databases, and other data sources using a syntax similar to SQL.
Example of LINQ:
List<int> numbers = new List<int> { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
// Query syntax
var evenNumbersQuery = from num in numbers
where num % 2 == 0
orderby num
select num;
// Method syntax
var evenNumbersMethod = numbers.Where(num => num % 2 == 0).OrderBy(num => num);
foreach (var num in evenNumbersMethod)
{
Console.WriteLine(num); // Output: 0, 2, 4, 6, 8
}
What are delegates and events in C#?
Delegates are type-safe function pointers that can reference methods with a specific signature. They are often used to implement callbacks or event handling. Events are a mechanism that allows an object to notify other objects when something happens. They are built upon delegates.
Example of Delegate and Event:
// Define a delegate
public delegate void MyEventHandler(object sender, EventArgs e);
// Define a class that raises an event
public class Publisher
{
public event MyEventHandler MyEvent;
public void RaiseEvent()
{
// Check if there are any subscribers before raising the event
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
// Define a class that subscribes to the event
public class Subscriber
{
public void Subscribe(Publisher publisher)
{
publisher.MyEvent += HandlerMethod;
}
private void HandlerMethod(object sender, EventArgs e)
{
Console.WriteLine("Event was raised!");
}
}
// Usage
Publisher pub = new Publisher();
Subscriber sub = new Subscriber();
sub.Subscribe(pub);
pub.RaiseEvent(); // Output: Event was raised!
What is the difference between ValueType and ReferenceType in C#?
Value types (like int, struct, bool) store their data directly. When you assign a value type to another variable, a copy of the data is made. Reference types (like class, string, array) store a reference (memory address) to the object’s data. When you assign a reference type, both variables point to the same object in memory.
Explain Garbage Collection in .NET.
Garbage Collection (GC) is an automatic memory management process in .NET. It identifies and reclaims memory that is no longer being used by the application. The GC works by tracking objects on the managed heap. When an object is no longer reachable from any active code (e.g., no references point to it), the GC marks it for deletion and reclaims its memory.
What is boxing and unboxing?
Boxing is the process of converting a value type to an object type. This involves allocating memory on the managed heap and copying the value type’s data into it. Unboxing is the reverse process, converting an object type back to a value type. This requires checking the object’s type at runtime to ensure it’s the correct value type.
Example of Boxing and Unboxing:
// Boxing
int myInt = 123;
object obj = myInt; // Boxing occurs here
// Unboxing
int unboxedInt = (int)obj; // Unboxing occurs here
Console.WriteLine(unboxedInt); // Output: 123
What are extension methods in C#?
Extension methods allow you to add new methods to existing types without modifying their original source code. They are static methods defined in a static class, and they use the this keyword before the first parameter to indicate that they extend a specific type.
Example of Extension Method:
public static class StringExtensions
{
public static int WordCount(this string str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
// Usage
string sentence = "This is a sample sentence.";
int count = sentence.WordCount(); // Calling the extension method
Console.WriteLine($"Word count: {count}"); // Output: Word count: 5
What is the using statement in C#?
The using statement is used to ensure that the Dispose() method of an object that implements IDisposable is called, even if an exception occurs. This is crucial for releasing unmanaged resources like file handles, database connections, or graphics objects.
Example of using statement:
using (StreamReader reader = new StreamReader("myFile.txt"))
{
string content = reader.ReadToEnd();
// Use the content
} // reader.Dispose() is automatically called here
What is an exception filter in C#?
Exception filters, introduced in C# 6.0, allow you to specify a condition that must be true for an exception handler (catch block) to execute. This can be useful for selectively catching exceptions based on certain criteria without having to inspect the exception object inside the catch block.
Example of Exception Filter:
try
{
// Some code that might throw an exception
throw new ArgumentException("Invalid argument");
}
catch (ArgumentException ex) when (ex.Message.Contains("Invalid"))
{
Console.WriteLine("Caught an ArgumentException with 'Invalid' in its message.");
}
catch (Exception ex)
{
Console.WriteLine($"Caught a general exception: {ex.Message}");
}
What is the difference between throw and throw ex?
When you use throw; within a catch block, it re-throws the original exception, preserving its original stack trace. When you use throw ex; (where ex is the caught exception object), it throws a new exception, and the stack trace will start from the line where throw ex; is called, potentially losing the original call stack information. It’s generally recommended to use throw; for re-throwing.
What is yield return?
The yield return statement is used in an iterator method or accessor to return the next element in a sequence. It allows you to create iterators that produce a sequence of values lazily, meaning values are generated on demand rather than all at once. This can significantly improve performance and reduce memory usage for large sequences.
Example of yield return:
public static IEnumerable<int> GenerateNumbers(int count)
{
for (int i = 0; i < count; i++)
{
yield return i * 2; // Lazily returns the next even number
}
}
// Usage
foreach (int num in GenerateNumbers(5))
{
Console.WriteLine(num); // Output: 0, 2, 4, 6, 8
}
What are structs in C#?
Structs are value types that are typically used for small, lightweight objects. They are stored directly where the variable is declared (on the stack for local variables, or inline within an object for fields). Unlike classes (reference types), structs cannot be null (unless they are nullable structs like int?), do not support inheritance from other structs or classes (though they can implement interfaces), and do not have destructors.
What is the IDisposable interface?
The IDisposable interface is a contract for types that encapsulate unmanaged resources. It defines a single method, Dispose(), which should be implemented to release these resources. The using statement is the idiomatic way to ensure Dispose() is called.
What are Generics in C#?
Generics provide a way to create type-safe classes, interfaces, methods, and delegates that can operate on a variety of types without casting. They allow you to write code that works with any type while ensuring type safety at compile time, reducing the need for runtime type checks and boxing/unboxing.
Example of Generics:
public class GenericList<T>
{
private List<T> _items = new List<T>();
public void Add(T item)
{
_items.Add(item);
}
public T Get(int index)
{
return _items[index];
}
}
// Usage
GenericList<int> intList = new GenericList<int>();
intList.Add(10);
int myInt = intList.Get(0);
GenericList<string> stringList = new GenericList<string>();
stringList.Add("Hello");
string myString = stringList.Get(0);
What is the difference between null and undefined?
In C#, there is no undefined keyword like in JavaScript. The closest concept is null. A variable of a reference type can be assigned null to indicate that it does not refer to any object. Value types cannot be null by default, but they can be made nullable by using the nullable type syntax (e.g., int?).
What is a Tuple in C#?
A Tuple is a data structure that allows you to group multiple fields of different types into a single object. Tuples are lightweight and can be used to return multiple values from a method without creating a custom class or struct.
Example of Tuple:
public (int Id, string Name) GetPersonInfo()
{
return (1, "Alice");
}
var person = GetPersonInfo();
Console.WriteLine($"ID: {person.Id}, Name: {person.Name}"); // Output: ID: 1, Name: Alice
What are record types in C#?
Introduced in C# 9, record types are a type of class that provides concise syntax for creating immutable data-holding objects. They automatically generate Equals(), GetHashCode(), ToString(), and equality members based on their properties, making them ideal for scenarios where you need value-based equality and immutability.
Example of record type:
public record Person(string FirstName, string LastName);
// Usage
Person person1 = new Person("John", "Doe");
Person person2 = new Person("John", "Doe");
Console.WriteLine(person1 == person2); // Output: True (value-based equality)
Console.WriteLine(person1.ToString()); // Output: Person { FirstName = John, LastName = Doe }
What is the Span<T> type?
Span<T> is a ref struct that provides a type-safe way to access a contiguous region of memory, whether it’s an array, a string, a native memory pointer, or a slice of another span. It’s designed for high-performance scenarios by minimizing memory allocations and enabling efficient data manipulation without copying.
What is the difference between ToList() and ToArray() in LINQ?
ToList() materializes a LINQ query into a List<T>, which is a dynamic collection that can grow. ToArray() materializes the query into an array, which has a fixed size once created. ToList() is generally more flexible if you expect to modify the collection later, while ToArray() might be slightly more performant if you only need a read-only, fixed-size collection and don’t plan to add or remove elements.
What is the using declaration in C#?
The using declaration (introduced in C# 8) allows you to declare and initialize a variable that implements IDisposable on a single line, and it will be disposed of automatically at the end of the scope (typically a block or method). It’s a more concise alternative to the using statement for common scenarios.
Example of using declaration:
void ProcessFile(string filePath)
{
using StreamReader reader = new StreamReader(filePath);
string content = reader.ReadToEnd();
// Use content
} // reader is disposed here automatically
What is the difference between ICollection<T>, IEnumerable<T>, and IList<T>?
IEnumerable<T>: Represents a sequence of elements that can be iterated over. It has a single method,GetEnumerator(). This is the most basic collection interface.ICollection<T>: Inherits fromIEnumerable<T>and adds methods for basic collection operations likeAdd(),Remove(),Clear(), and properties likeCount.IList<T>: Inherits fromICollection<T>and adds indexed access (like[index]) and methods likeInsert(),RemoveAt(), andIndexOf().
What is a finalizer (or destructor) in C#?
A finalizer is a special method in a class that is called by the garbage collector just before an object is destroyed. It’s used to clean up unmanaged resources. Finalizers are not guaranteed to be called at any specific time, and they add overhead. It’s generally preferred to use IDisposable and the using statement for deterministic resource management.
What is the difference between checked and unchecked contexts?
The checked context enforces overflow checking for arithmetic operations. If an overflow occurs within a checked block, an OverflowException is thrown. The unchecked context (which is the default) does not perform overflow checking, meaning arithmetic operations that exceed the type’s bounds will wrap around without throwing an exception.
Example of checked context:
int a = int.MaxValue;
int b = 10;
try
{
checked
{
int sum = a + b; // This will throw an OverflowException
}
}
catch (OverflowException)
{
Console.WriteLine("Overflow occurred!");
}
What is the dynamic keyword in C#?
The dynamic keyword allows you to bypass compile-time type checking. Operations on dynamic objects are resolved at runtime. This can be useful when working with COM objects, dynamic languages (like IronPython or IronRuby), or when dealing with data where the type is not known until runtime. However, it sacrifices compile-time safety and can lead to runtime errors if not used carefully.
What is the ref and out keyword?
ref: Passes a parameter by reference. The variable passed must be initialized before being passed to the method. Changes made to the parameter inside the method affect the original variable.out: Also passes a parameter by reference, but the variable passed does not need to be initialized. The method is required to assign a value to theoutparameter before it returns.
What is the in keyword?
The in keyword is used to pass a parameter by reference, but it guarantees that the method will not modify the parameter’s value. It’s similar to ref in that it avoids copying the argument, but it provides read-only semantics for the method.
What is the readonly keyword?
The readonly keyword can be applied to fields of a class or struct. A readonly field can only be assigned a value during its declaration or within the constructor of the containing class or struct. This helps enforce immutability.
What is struct inheritance?
Structs cannot inherit from other structs or classes. However, they can implement interfaces. This is a key difference from classes, which can inherit from a single base class and implement multiple interfaces.
What is the var keyword?
The var keyword is used for implicitly typed local variables. The compiler infers the type of the variable based on the expression used to initialize it. It does not mean the variable is dynamic; it’s just a compile-time type inference.
Example of var:
var message = "Hello, World!"; // Compiler infers message is of type string
var count = 100; // Compiler infers count is of type int
What is the difference between Equals() and == operator for reference types?
By default, the == operator for reference types checks for reference equality (i.e., if two variables point to the exact same object in memory). The Equals() method, by default, also checks for reference equality. However, many classes (like string or custom classes that override it) override the Equals() method to perform value equality (i.e., checking if the contents or properties of the objects are the same).
What is the is operator?
The is operator checks if an object is compatible with a given type. It returns true if the object can be cast to the type without throwing an exception, and false otherwise. It’s often used in conjunction with pattern matching.
Example of is operator:
object obj = "Hello";
if (obj is string)
{
Console.WriteLine("Object is a string.");
}
What is the as operator?
The as operator attempts to cast an object to a specified type. If the cast is successful, it returns the cast object. If the cast is not possible, it returns null instead of throwing an exception. It can only be used with reference types or nullable value types.
Example of as operator:
object obj = 123;
string str = obj as string; // str will be null because 123 is not a string
if (str == null)
{
Console.WriteLine("Cast to string failed.");
}
What is the difference between dispose() and finalize()?
Dispose() is a method from the IDisposable interface, called explicitly (usually via a using statement) to deterministically release unmanaged resources. Finalize() (or a destructor) is a method called by the garbage collector non-deterministically to clean up resources when an object is no longer reachable. Dispose() is preferred for resource management because it guarantees timely release.
What are struct constraints in Generics?
A struct constraint means that the type parameter must be a value type (a struct). This is used to ensure that the generic code can safely perform value-type operations.
Example of struct constraint:
public void ProcessValue<T>(T value) where T : struct
{
// T is guaranteed to be a struct
}
What is Null Propagation (?. and ?[])?
Null propagation operators provide a concise way to access members or elements of an object that might be null.
?.: If the expression to the left of?.isnull, the entire expression evaluates tonullwithout throwing aNullReferenceException.?[]: Similar to?.but for accessing elements in collections or arrays.
Example of Null Propagation:
string name = null;
int? length = name?.Length; // length will be null
What is Pattern Matching in C#?
Pattern matching allows you to check the type of an object and extract data from it in a single operation. It enhances the is and as operators, and is used extensively with switch statements and expressions.
Example of Pattern Matching:
object obj = "hello";
switch (obj)
{
case string s when s.Length > 3:
Console.WriteLine($"Long string: {s}");
break;
case string s:
Console.WriteLine($"Short string: {s}");
break;
case int i:
Console.WriteLine($"Integer: {i}");
break;
default:
Console.WriteLine("Unknown type");
break;
}
What is the nameof operator?
The nameof operator returns the name of the operand (variable, type, method, etc.) as a string. It’s useful for logging, error messages, or reflection, as it provides compile-time safety – if you rename the operand, the string will be updated automatically.
Example of nameof operator:
string personName = "Alice";
Console.WriteLine(nameof(personName)); // Output: personName
What are local functions?
Local functions are methods defined within the body of another method. They are only accessible within the containing method. They can capture variables from the enclosing scope, similar to lambda expressions, but offer the structure of a full method.
Example of Local Function:
public void Greet(string name)
{
void PrintGreeting()
{
Console.WriteLine($"Hello, {name}!"); // Captures 'name' from outer scope
}
PrintGreeting();
}
What is the difference between Task and Task<TResult>?
Task represents an asynchronous operation that does not return a value. Task<TResult> represents an asynchronous operation that returns a value of type TResult.
What is the ConfigureAwait(false) method used for?
When awaiting a Task, ConfigureAwait(false) tells the runtime not to resume the continuation on the original synchronization context. This is typically used in library code to avoid deadlocks and improve performance by not forcing continuations back to the UI thread or other specific contexts.
What are ValueTask and ValueTask<TResult>?
ValueTask and ValueTask<TResult> are value types that provide an alternative to Task and Task<TResult>. They are designed to improve performance in scenarios where asynchronous methods frequently complete synchronously. By using a struct, ValueTask can avoid heap allocations for synchronous results, leading to reduced garbage collection pressure.
What is the IAsyncEnumerable<T> interface?
IAsyncEnumerable<T> is an interface that represents a sequence of elements that can be iterated over asynchronously. It’s the asynchronous counterpart to IEnumerable<T> and is used with the await foreach syntax.
Example of IAsyncEnumerable<T>:
public async IAsyncEnumerable<int> GenerateNumbersAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100, cancellationToken); // Simulate async work
yield return i * 2;
}
}
// Usage
await foreach (var num in GenerateNumbersAsync())
{
Console.WriteLine(num);
}
What is the using directive for static classes?
The using static directive allows you to use the static members of a type directly without having to qualify them with the type name. This can make code more concise, especially when working with utility classes.
Example of using static:
using static System.Console;
class Program
{
static void Main(string[] args)
{
WriteLine("Hello from static Console!"); // No need for Console.WriteLine
}
}
What is collection expression in C#?
Collection expressions (introduced in C# 12) provide a new, more concise syntax for creating and initializing collections like arrays, lists, and dictionaries, similar to how object initializers work for objects.
Example of Collection Expression:
// Old way
List<int> numbersList = new List<int> { 1, 2, 3 };
int[] numbersArray = new int[] { 1, 2, 3 };
// New way (C# 12)
List<int> numbersList = [1, 2, 3];
int[] numbersArray = [1, 2, 3];
What is primary constructors for classes and structs?
Primary constructors (introduced in C# 12 for classes and structs) are declared directly in the class/struct header. They allow you to define constructor parameters that are available as fields or properties within the class/struct body, simplifying common initialization patterns.
Example of Primary Constructor:
public class Person(string name, int age)
{
public string Name { get; } = name;
public int Age { get; } = age;
public void Display()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
// Usage
var p = new Person("Bob", 30);
p.Display(); // Output: Name: Bob, Age: 30