C# Questions
Interview-ready C# questions covering .NET, OOP, LINQ, and real-world coding.
1 What is C# and what are its key features?
What is C# and what are its key features?
What is C#?
C# (pronounced "C sharp") is a modern, high-level, object-oriented programming language created by Microsoft as part of the .NET ecosystem. It is designed to be simple, expressive, and type-safe. C# is used for a wide range of applications — desktop, web, cloud, mobile, games (Unity), and more — and runs on the Common Language Runtime (CLR), which provides services such as memory management, security, and exception handling.
Core design goals included safety, productivity, and performance. C# balances developer ergonomics with strong static typing and platform capabilities provided by the .NET runtime.
Key features — quick table
| Feature | What it gives you |
|---|---|
| Static typing | Compile-time type checking and better tooling (IntelliSense, refactoring) |
| Object-oriented | Classes, interfaces, inheritance, polymorphism, encapsulation |
| Garbage collection | Automatic memory management via the CLR |
| Generics | Type-safe reusable code (List<T>, Dictionary<TKey,TValue>) |
| LINQ | Declarative querying over collections and data sources |
| async/await | Cleaner asynchronous programming for IO-bound code |
| Rich standard library | Wide APIs for IO, networking, threading, cryptography, etc. |
| Cross-platform .NET | Run on Windows, Linux, macOS via .NET 5/6/7+ |
| Interoperability | P/Invoke and COM interop for native/legacy integration |
Expanded explanations
- Strong static typing: C#’s compile-time type system reduces a large class of bugs and enables powerful editor tooling.
- Language productivity features: Properties, pattern matching, records, tuples, local functions, and top-level statements (in newer versions) reduce boilerplate.
- Asynchronous programming: async/await makes asynchronous code read like synchronous code; Tasks are the primary abstraction for async operations.
- LINQ: Language Integrated Query provides a unified, composable syntax to work with in-memory collections, databases (via providers), XML, etc.
- Cross-platform runtime: Modern .NET (5+) unifies the platform so C# apps run on multiple OSes with a single codebase.
Small example — simple C# class
using System;
namespace Demo
{
public class Greeter
{
public string Name { get; set; }
public Greeter(string name) => Name = name;
public void Greet() => Console.WriteLine($"Hello, {Name}!");
}
class Program
{
static void Main()
{
var g = new Greeter("World");
g.Greet();
}
}
}When to choose C#
Choose C# when you need a strongly-typed, productive language that integrates with a mature runtime and ecosystem, whether you build web APIs, enterprise services, desktop apps, games (Unity), or cloud-native systems.
2 Explain the basic structure of a C# program and the role of the Main method.
Explain the basic structure of a C# program and the role of the Main method.
Program structure overview
A typical C# program uses a namespace to group types and one or more type definitions (classes, structs, enums, interfaces). Execution begins at a single entry point — the Main method — unless you're building a library or running code via scripting/top-level statements.
Common parts of a C# program
- using directives — import namespaces (
using System;). - namespace — logical grouping of related types.
- types — classes, structs, enums, interfaces define program behavior and data.
- Main method — program entry point where execution starts.
Variants of Main
The Main method can have several signatures. Common variants include:
| Signature | Meaning |
|---|---|
static void Main() | Simple entry point; doesn't return exit code. |
static int Main() | Returns an int exit code to the OS. |
static async Task Main() | Allows use of await at top-level in console apps (C# 7.1+). |
static async Task<int> Main() | Async with an int exit code. |
Top-level statements
From C# 9 onward, you can write top-level statements (no explicit Main) for small programs or learning examples. The compiler generates Main behind the scenes.
Example — classic console app
using System;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello from Main!");
if (args.Length > 0)
Console.WriteLine($"First arg: {args[0]}");
}
}
}Example — async entry point
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await Task.Delay(100); // simulate async work
Console.WriteLine("Finished async work");
}
}Why Main matters
Main is the single starting place for the runtime to begin executing your program. It controls application initialization, argument parsing, and can await async startup logic. For frameworks (ASP.NET Core, libraries), the framework usually controls startup and you provide configuration/entry wiring instead of a typical Main implementation.
3 What are the built-in data types in C# and how do int and Int32 differ (if at all)?
What are the built-in data types in C# and how do int and Int32 differ (if at all)?
Built-in (primitive) types
C# provides a set of convenient aliases for commonly-used CLR types. These aliases are part of the language and map directly to System.* types provided by the runtime.
| C# alias | CLR type | Description |
|---|---|---|
bool | System.Boolean | Boolean true/false |
byte | System.Byte | Unsigned 8-bit integer |
sbyte | System.SByte | Signed 8-bit integer |
short | System.Int16 | Signed 16-bit integer |
ushort | System.UInt16 | Unsigned 16-bit integer |
int | System.Int32 | Signed 32-bit integer |
uint | System.UInt32 | Unsigned 32-bit integer |
long | System.Int64 | Signed 64-bit integer |
ulong | System.UInt64 | Unsigned 64-bit integer |
float | System.Single | Single-precision floating point |
double | System.Double | Double-precision floating point |
decimal | System.Decimal | High-precision decimal for financial calculations |
char | System.Char | UTF-16 character |
string | System.String | Immutable sequence of characters |
object | System.Object | Base type of all types |
int vs Int32
int is a C# language alias for System.Int32. They are identical at compile-time and runtime — using either is a matter of style/clarity. Some projects prefer CLR type names in interop-heavy code; most code uses aliases for brevity.
Example demonstrating alias equivalence
int a = 42;
System.Int32 b = 42;
Console.WriteLine(a.GetType()); // System.Int32
Console.WriteLine(b.GetType()); // System.Int32
Console.WriteLine(a == b); // TrueNullable & composite types
C# supports nullable value types (e.g., int?) and reference types (via nullable reference annotations). Composite types include arrays (int[]), tuples ((int, string)), and generics (List<T>).
Practical tip
Prefer the language aliases (intstringbool) for readability in most code; use CLR names when the exact CLR type needs emphasis or for documentation/interoperability clarity.
4 What is the difference between value types and reference types?
What is the difference between value types and reference types?
High-level distinction
In C#, types are broadly categorized into value types and reference types. The distinction affects memory layout, lifetime, assignment semantics, and performance characteristics.
| Aspect | Value type | Reference type |
|---|---|---|
| Examples | int, double, bool, DateTime, struct | class, string, object, arrays, delegates |
| Storage | Stored directly (stack or inline in other objects) | Stored on the heap; variable holds a reference |
| Assignment | Copies the value (deep copy for fields) | Copies the reference (both refer to same object) |
| Default nullability | Cannot be null (unless Nullable<T>) | Can be null (reference may point to nothing) |
| Garbage collection | No; storage reclaimed when scope ends | Managed by GC |
| Typical use | Small, immutable, short-lived data | Complex objects, identity, large state |
Code example showing assignment behavior
struct PointStruct { public int X; public int Y; }
class PointClass { public int X; public int Y; }
// Value type copy
var a = new PointStruct { X = 1, Y = 2 };
var b = a; // b is a copy
b.X = 100; // a.X remains 1
// Reference type copy
var c = new PointClass { X = 1, Y = 2 };
var d = c; // d references same object
d.X = 100; // c.X is now 100Boxing and unboxing
When a value type is converted to object or an interface type, it gets "boxed" — copied into an object on the heap. Unboxing extracts the value back. Boxing allocates and can be a source of performance overhead.
When to prefer each
- Use value types/structs for small, immutable data where allocation and GC overhead should be avoided (e.g.,
PointTimestamp). Keep them small (rule of thumb: under 16 bytes when possible). - Use reference types/classes when objects have identity, mutable shared state, large graphs, or when inheritance/polymorphism is required.
Performance considerations
Value types reduce GC pressure (no heap allocation) but copying large structs is expensive; reference types avoid copying but allocate on the heap and increase GC work. Profile before optimizing and consider ref struct or Span<T> for advanced scenarios.
5 What are nullable types and how do nullable reference types (C# 8+) work?
What are nullable types and how do nullable reference types (C# 8+) work?
Nullable value types
By default, value types (like int) cannot be null. C# provides Nullable<T> (shorthand T?) to represent value types that can be null:
int? maybe = null;
if (maybe.HasValue)
{
int v = maybe.Value; // safe because HasValue was checked
}
// or using null-coalescing
int valueOrDefault = maybe ?? 0;The ?? operator returns the left operand if it isn't null; otherwise returns the right operand.
Nullable reference types (C# 8+)
Historically, reference types could be null and the compiler did not help much in preventing null-reference exceptions. C# 8 introduced nullable reference types as an opt-in feature that treats reference types as non-nullable by default and uses annotations and static analysis to warn about potential null dereferences.
| Notation | Meaning |
|---|---|
string | Non-nullable reference type (compiler warns if it may be null) |
string? | Nullable reference type — may be null (compiler enforces null checks) |
How to enable
Enable nullable analysis in project file (<Nullable>enable</Nullable>) or use #nullable enable in a file. When enabled, the compiler emits warnings when you assign null to a non-nullable reference or dereference a nullable reference without a null-check.
Example — nullable reference types in action
#nullable enable
public class Person
{
public string Name { get; set; } // compiler expects non-null
public string? Nickname { get; set; } // may be null
}
public void Print(Person p)
{
Console.WriteLine(p.Name.Length); // safe - compiler expects Name non-null
if (p.Nickname != null)
Console.WriteLine(p.Nickname.Length); // safe after null-check
}
#nullable disableImportant notes about nullable reference types
- They are a compile-time, static-analysis feature. Runtime behavior does not change: nullable reference annotations do not add runtime checks by default.
- You can use the null-forgiving operator
!to tell the compiler "I know this isn't null":string s = possiblyNull!;— this suppresses warnings but should be used sparingly. - Attributes such as
[MaybeNull][NotNullWhen(true)], etc., provide finer-grained control for library authors.
Comparison table — value nullable vs reference nullable
| Aspect | Value Nullable (T?) | Nullable Reference (T?, C# 8+) |
|---|---|---|
| Runtime representation | Nullable<T> wraps value and HasValue flag | No runtime wrapper; just normal reference which may be null |
| Compile-time help | Type system enforces non-nullability for value types by design | Static analysis warns about possible nulls when enabled |
| How to check | HasValue / compare to null | null-checks, pattern matching, or ! to suppress |
Practical tips
- Prefer enabling nullable reference types in new projects — they catch many null-related bugs early.
- When migrating large codebases, consider
<Nullable>enablewith gradual fixes to suppress warnings incrementally. - Use
???.(null-conditional), and! (null-forgiving)thoughtfully.
6 What are namespaces and why are they used?
What are namespaces and why are they used?
What is a namespace?
A namespace in C# is a logical container for types (classes, structs, enums, interfaces, delegates). Namespaces do not affect runtime behavior directly — they are a compile-time organization mechanism that helps avoid naming conflicts and provides a clear structure for large codebases.
Why use namespaces?
- Avoid name collisions: Two libraries can provide a type with the same name but different namespaces (e.g.,
CompanyA.Logging.LoggervsCompanyB.Logging.Logger). - Organize code: Group related functionality (e.g.,
MyApp.DataMyApp.ServicesMyApp.UI). - Improve readability: Namespaces make it easier to find and understand where a type belongs.
- Enable scoped using directives: You can import only the namespaces you need with
usingto reduce verbosity.
Namespace syntax & examples
namespace MyCompany.MyProduct.Data
{
public class UserRepository { /* ... */ }
}
namespace MyCompany.MyProduct.Services
{
public class UserService { /* ... */ }
}You can reference types with using or fully-qualified names:
using MyCompany.MyProduct.Data;
var repo = new UserRepository();
// or
var repo2 = new MyCompany.MyProduct.Data.UserRepository();Nested namespaces and shorthand
Namespaces can be nested or declared using the dot syntax. C# also supports the file-scoped namespace (C# 10+) to reduce indentation:
// Traditional
namespace A.B.C
{
class X { }
}
// File-scoped (C# 10+)
namespace A.B.C;
class X { }Best practices
- Name namespaces to reflect company/project and module (e.g.,
Contoso.Payment.Processing). - Keep namespaces focused and avoid deeply nested types unless necessary.
- Organize folder structure to mirror namespaces for discoverability.
- Be explicit when importing external types that may collide (use aliasing:
using LogA = CompanyA.Logging.Logger;).
Example — aliasing to disambiguate
using LogA = CompanyA.Logging.Logger;
using LogB = CompanyB.Logging.Logger;
LogA a = new LogA();
LogB b = new LogB(); 7 What is the var keyword and when should you use type inference?
What is the var keyword and when should you use type inference?
What does var do?
In C#, var signals that the compiler should infer the variable's static type from the expression on the right-hand side at compile time. The actual type is determined during compilation — this is not dynamic typing.
Examples
var i = 10; // int
var name = "Alice"; // string
var list = new List<string>(); // List
// Complex LINQ expression
var result = people.Where(p => p.Age > 30).Select(p => p.Name); When to use var
- Use
varwhen the type is obvious from the right-hand side (e.g.,var stream = File.OpenRead(path);). - Use it to avoid redundancy (e.g.,
Dictionary<int,string> d = new Dictionary<int,string>()vsvar d = new Dictionary<int,string>()). - Use it for complex anonymous or LINQ types where the explicit type is cumbersome or not available (anonymous types).
When to avoid var
- Avoid when the right-hand side does not reveal the type clearly (e.g.,
var x = Get();— what doesGetreturn?). - When explicitness improves readability for the team or public APIs.
Rules and behavior
- The type must be determinable at compile-time;
varcannot be used for fields (only local variables), method parameters, or return types. - Using
vardoes not make the variable dynamically typed; it still has a static compile-time type.
Examples comparing clarity
// Clear
var orders = GetOpenOrders(); // GetOpenOrders returns IEnumerable
// Unclear — prefer explicit
var data = Load(); // what is Load() returning? better: OrderDto[] data = Load(); Style guidance
Follow your team's style guide. A common rule: use var when the type is obvious or verbose; prefer explicit types when they aid understanding. Consistency across a codebase is often more valuable than rigid rules.
8 Explain boxing and unboxing and the pitfalls.
Explain boxing and unboxing and the pitfalls.
What is boxing?
Boxing is the process of converting a value type (e.g., intstruct) to a reference type (object or an interface it implements). Boxing creates a new object on the heap and copies the value into it.
What is unboxing?
Unboxing extracts the value type from the boxed object. It requires an explicit cast and will throw InvalidCastException if the runtime type does not match.
Example
int x = 123; // value type
object o = x; // boxing — allocates an object on the heap
int y = (int)o; // unboxing — cast back to intWhy it matters
- Performance: Boxing allocates on the managed heap and triggers GC pressure. Frequent boxing/unboxing inside hot loops or high-throughput code can degrade performance.
- Copy semantics: Boxing copies the value. Modifying the boxed object does not change the original value type and vice versa.
- Type safety: Unboxing requires the exact value type; casting to the wrong type raises an exception.
Pitfalls & how to avoid them
- Unintentional boxing: Avoid storing value types in non-generic collections (pre-.NET 2.0) like
ArrayList. Use generic collections (e.g.,List<int>) which avoid boxing. - Boxing in APIs: Be careful with APIs that take
objectorparams object[]— passing value types will cause boxing. - Tooling: Use analyzers and profilers to detect boxing hotspots (Roslyn analyzers like IDE0043 or performance profilers).
Alternatives & improvements
- Use generics to avoid boxing and preserve type safety (e.g.,
List<T>). - For advanced scenarios, use
Span<T>Memory<T>, andref structto work with memory efficiently without heap allocations.
9 What is type casting and what are implicit vs explicit casts?
What is type casting and what are implicit vs explicit casts?
What is casting?
Casting is converting a value from one type to another. There are two high-level forms: implicit (safe, automatic) and explicit (may lose information or fail).
Implicit casts
Implicit casts are conversions the compiler can guarantee to be safe (no data loss or exceptions). Examples:
int i = 42;
long l = i; // implicit: int → long
float f = i; // implicit: int → floatExplicit casts
Explicit casts are required when the conversion is narrowing or might fail. You must use a cast operator:
double d = 3.14;
int n = (int)d; // explicit: fractional part truncated
object o = "hello";
string s = (string)o; // explicit reference castReference type casting
For reference types, casts check the runtime type and will throw InvalidCastException if the object cannot be converted. Use the as operator for a safe cast that returns null on failure:
object o = GetSomething();
MyClass m1 = (MyClass)o; // throws if incompatible
MyClass m2 = o as MyClass; // null if incompatiblePattern matching for safer casts
Modern C# favors pattern matching as a concise and safe way to check and use types:
if (o is MyClass mc)
{
// use mc safely
}
// or switch
switch (o)
{
case MyClass mc:
// use mc
break;
}Custom conversions
Types can provide custom implicit or explicit conversion operators (operator implicit/explicit) to control how conversion behaves. Use them sparingly to avoid surprising implicit conversions.
When to use which
- Use implicit casts where there is no risk of data loss and the conversion is obvious.
- Require explicit casts for narrowing conversions or where failure is possible; consider
asoristo check types safely.
10 Difference between the assignment operator '=' and Equals()/==?
Difference between the assignment operator '=' and Equals()/==?
Assignment vs comparison
The = operator assigns the right-hand value to the left-hand variable. It does not perform comparison. Comparisons are done with == (equality operator) or Equals() (method).
= — assignment
int a = 5; // assigns 5 to a
string s = "hi"; // assigns a reference to s== — equality operator
== checks equality and can be overloaded by types. For built-in value types, it checks value equality (e.g., int); for string, it is overloaded to perform content comparison. For reference types that do not overload it, == checks reference equality (same object).
int x = 5;
int y = 5;
Console.WriteLine(x == y); // True (value equality)
string a = "abc";
string b = new string(new[] { 'a','b','c' });
Console.WriteLine(a == b); // True (string overrides == to compare contents)
object o1 = new object();
object o2 = new object();
Console.WriteLine(o1 == o2); // False (different references)Equals() — virtual method
Equals(object) is defined on System.Object and can be overridden to define semantic equality. Value types get a default implementation that compares fields, while reference types often override for content equality.
public class Person
{
public string Name { get; set; }
public override bool Equals(object obj)
{
return obj is Person p && p.Name == Name;
}
public override int GetHashCode() => Name?.GetHashCode() ?? 0;
}
var p1 = new Person { Name = "Alice" };
var p2 = new Person { Name = "Alice" };
Console.WriteLine(p1 == p2); // False unless == overloaded
Console.WriteLine(p1.Equals(p2)); // True because Equals overriddenGetHashCode
When overriding Equals, always override GetHashCode to preserve hash-based collection invariants (e.g., DictionaryHashSet).
Guidelines
- Use
=to assign values,==orEquals()to compare. Understand whether==is overloaded for the types you compare. - Prefer
EqualsorObject.Equalswhen you want semantic equality that is consistent across types; use==for common patterns but be mindful of operator overloads. - When implementing equality for custom types, implement
IEquatable<T>, overrideEquals(object)andGetHashCode, and consider overloading==and!=for consistency.
11 What are access modifiers in C# (public, private, protected, internal)?
What are access modifiers in C# (public, private, protected, internal)?
Overview
Access modifiers determine which code can see or use a type or a member (field, property, method, nested type). They are a key part of encapsulation and API design — by choosing the right accessibility you express intent and reduce accidental coupling.
Common access modifiers and meanings
| Modifier | Where visible | Typical use |
|---|---|---|
public | Any code that can reference the assembly/type | Public API surface, libraries, DTOs |
private | Only within the declaring type | Implementation details, helper fields/methods |
protected | Declaring type and derived types (inheritance) | Allow subclasses to access behavior/state |
internal | Any code in the same assembly | Types/members for internal use within a project |
protected internal | Either derived types OR same assembly | Broad access for derived types and intra-assembly consumers |
private protected | Derived types in the same assembly | Restrict protected access to same-assembly inheritance |
Defaults and special cases
- Member (field/method/property) default accessibility inside a class is
private. - Top-level (namespace-level) class default is
internalif no modifier is given. - Nested types can use the same modifiers and follow the same rules; a private nested type is only visible inside its containing type.
Example: practical usage
namespace MyApp.Core
{
internal class InternalHelper { /* not visible outside this assembly */ }
public class Account
{
private decimal balance; // implementation detail
protected void ChangeBalance(decimal delta) { balance += delta; }
public decimal GetBalance() => balance; // public API
}
public class VipAccount : Account
{
public void ApplyReward() => ChangeBalance(100); // allowed because protected
}
}Guidelines & best practices
- Prefer the least permissive accessibility that still allows the required usage — start private and widen only when necessary.
- Use
internalfor implementation types you don’t want to expose from a library (combine withInternalsVisibleTofor test assemblies if needed). - Avoid exposing fields as
public; prefer properties to preserve encapsulation and allow future validation. - Use
protectedwhen you expect derived classes to need access, but be careful — protected members create a tighter coupling with subclasses.
When to use the combined modifiers
protected internal is handy when a type should be extensible by subclasses across assemblies but you also want to allow same-assembly code to access internals. private protected is more restrictive: only subclasses that live in the same assembly can use the member — useful for frameworks that allow limited extension points.
12 What is a static class or member? When are they useful?
What is a static class or member? When are they useful?
Concept
A static member (field, property, method, constructor) belongs to the type itself rather than to any particular object instance. A static class is a class that can only contain static members and cannot be instantiated.
When to choose static
- Utility or helper functions that have no instance state (e.g.,
Math, string helpers). - Singleton-like behavior where a single shared resource or manager is appropriate (but prefer explicit singletons or dependency injection for testability).
- Constants or cached, read-only data shared across the app.
Static members: examples and behavior
public static class MathHelpers
{
// static method — no instance required
public static int Square(int x) => x * x;
// static field — shared across all code
public static int CallCount;
// static constructor — runs once before first use
static MathHelpers() => CallCount = 0;
}
// Usage:
int s = MathHelpers.Square(5);
MathHelpers.CallCount++;Static constructor
Static constructors are parameterless and run automatically before the first access to any static member or before the first instance is created (if the type is non-static). They are useful for one-time initialization.
Static vs instance trade-offs
| Aspect | Static | Instance |
|---|---|---|
| State lifetime | Application/domain lifetime (shared) | Per-object lifetime |
| Testability | Harder to mock/test without wrappers | Easier to inject via DI |
| Thread-safety | Requires explicit synchronization for mutable state | Can avoid shared-state issues |
Pitfalls & best practices
- Avoid hidden global state: mutable static fields act like globals — they make reasoning and testing harder. Prefer immutable static data when possible.
- Prefer dependency injection: For services or components that you may want to mock in tests, prefer instance types registered with DI containers rather than static singletons.
- Make thread-safety explicit: If static members hold mutable state, protect access with locks or use concurrent collections.
- Use static classes for extensions: Extension methods must live in a static class (
public static class StringExtensions { public static bool IsNullOrWhiteSpace(this string s) { ... } }).
When a static class is preferred
Use a static class for stateless helper methods (pure functions) or constants. For services with complex lifecycle, configuration, or dependencies, prefer instance-based design with DI.
13 Define OOP principles with C# examples (encapsulation, inheritance, polymorphism, abstraction).
Define OOP principles with C# examples (encapsulation, inheritance, polymorphism, abstraction).
OOP principles — short summary
Object-oriented programming (OOP) is built on four core principles: encapsulationinheritancepolymorphism, and abstraction. C# provides language features that map directly to these principles.
Encapsulation
Encapsulation hides internal details and exposes a well-defined public surface. Use access modifiers and properties to control access.
public class BankAccount
{
private decimal balance; // hidden
public void Deposit(decimal amount) { if (amount > 0) balance += amount; }
public decimal GetBalance() => balance;
}Inheritance
Inheritance lets a class reuse and extend behavior from a base class. C# supports single class inheritance (a class can inherit from one other class) and multiple interface implementation.
public class Animal { public virtual void Speak() => Console.WriteLine("..."); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("Woof"); }Polymorphism
Polymorphism allows code to operate on base types while concrete behavior is provided by derived types (via virtual/override or interface implementation).
void MakeItSpeak(Animal a) { a.Speak(); }
MakeItSpeak(new Dog()); // prints Woof — runtime dispatch chooses overrideAbstraction
Abstraction reduces complexity by exposing only necessary details via interfaces or abstract classes. It decouples callers from concrete implementations.
public interface IRepository
{
void Add(T item);
T Get(int id);
}
public class SqlRepository : IRepository { /* implementation */ }
// Code depends on IRepository<T>, not SqlRepository<T>
Combined example & explanation
public abstract class Shape
{
public abstract double Area(); // abstraction
}
public class Circle : Shape
{
private readonly double radius; // encapsulation
public Circle(double r) => radius = r;
public override double Area() => Math.PI * radius * radius; // polymorphism/inheritance
}
void PrintArea(Shape s) => Console.WriteLine(s.Area());
PrintArea(new Circle(2));Design guidance
- Use encapsulation to keep invariants and hide implementation details.
- Prefer composition over inheritance unless a clear "is-a" relationship exists.
- Design to interfaces (abstraction) to increase testability and flexibility.
- Keep polymorphic hierarchies shallow and focused to avoid fragile base-class problems.
14 What is a class vs a struct, and when should you use each?
What is a class vs a struct, and when should you use each?
Fundamental difference
The primary difference is that classes are reference types and their variables hold references to objects on the heap, while structs are value types and variables hold the data directly (copy semantics).
| Aspect | Class (reference type) | Struct (value type) |
|---|---|---|
| Storage | Heap (managed by GC) | Stack or inline within other objects |
| Assignment | Copies reference (shared object) | Copies entire value (independent) |
| Inheritance | Supports single inheritance | Cannot inherit from another struct (but can implement interfaces) |
| Default constructor | Can define parameterless constructor (behavior depends on C# version) | Always has an implicit parameterless constructor that initializes fields to default |
| Use-case | Large, mutable, identity-bearing objects | Small, immutable, short-lived data (coordinates, small DTOs) |
Code example showing behavior
struct PointStruct { public int X; public int Y; }
class PointClass { public int X; public int Y; }
var ps1 = new PointStruct { X = 1, Y = 2 };
var ps2 = ps1; // copy
ps2.X = 10; // ps1.X still 1
var pc1 = new PointClass { X = 1, Y = 2 };
var pc2 = pc1; // reference copy
pc2.X = 10; // pc1.X now 10
When to prefer a struct
- Small (generally a few fields), immutable, value-like types (point, RGB color, small tuple).
- When you want to avoid heap allocation and GC pressure for lots of short-lived objects.
- When the semantics are value equality rather than identity.
When to prefer a class
- Objects with identity, mutable state, or large memory footprint.
- When you need inheritance and polymorphism beyond what interfaces provide.
- If copying the data would be expensive or confusing.
Practical recommendations
- Keep structs small and immutable — large structs incur copy overhead.
- Prefer classes for most domain models and service objects.
- Use structs for performance-sensitive, low-level types and after measuring that heap allocations are a real problem.
15 Explain inheritance; does C# support multiple inheritance?
Explain inheritance; does C# support multiple inheritance?
What is inheritance?
Inheritance is an OOP mechanism where a derived (child) class reuses and extends the behavior and state of a base (parent) class. It promotes code reuse and models "is-a" relationships (e.g., a Dog is an Animal).
C# inheritance rules
- A class in C# can inherit from one other class (single inheritance).
- A class can implement multiple interfaces.
- The
basekeyword accesses members or constructors of the immediate base class. - You can use
virtualon base methods andoverrideon derived methods to change behavior at runtime (polymorphism).
Does C# support multiple inheritance?
No — C# does not allow a class to inherit from more than one class. This avoids the classic "diamond problem" of multiple inheritance (ambiguity about which base implementation to use). Instead, C# encourages composition and multiple interface implementation to gain flexibility without the complexities of multiple base classes.
Example: single inheritance + interfaces
public interface IMovable { void Move(); }
public class Animal { public virtual void Speak() => Console.WriteLine("..."); }
public class Dog : Animal, IMovable
{
public override void Speak() => Console.WriteLine("Woof");
public void Move() => Console.WriteLine("Dog runs");
}
// Usage
Animal a = new Dog();
a.Speak(); // Woof (override)
(Dog) a).Move();
Alternatives to multiple inheritance
- Interfaces: Implement many interfaces to expose behaviors. Since C# 8, interfaces can have default implementations, but they still avoid the full complexity of multiple inheritance.
- Composition: Encapsulate behavior in other objects and delegate calls. "Has-a" relationships reduce coupling compared to "is-a" inheritance.
- Mixins via generics or code generation: For advanced scenarios you can simulate mixin-like behavior, but prefer clear, maintainable designs.
Design guidance
- Use inheritance when there is a clear subtype relationship and the derived class truly is a specialized form of the base.
- Prefer composition over inheritance when you need to combine behaviors or avoid fragile base-class dependencies.
- Keep inheritance hierarchies shallow and cohesive; deep hierarchies are harder to maintain and understand.
16 What is polymorphism? Explain method overriding vs overloading.
What is polymorphism? Explain method overriding vs overloading.
Polymorphism — high level
Polymorphism (from Greek, "many forms") is the ability of code to treat different types uniformly while allowing each type to provide its own behavior. In C# polymorphism is a cornerstone of OOP and enables writing flexible, extensible systems.
There are two common forms often discussed in interviews:
- Compile-time (static) polymorphism — method overloading, operator overloading, generic specialization.
- Runtime (dynamic) polymorphism — method overriding via virtual/override and interface implementation (late binding).
Method overloading (compile-time)
Overloading means defining multiple methods with the same name but different parameter lists within the same type. The compiler chooses the correct overload based on the compile-time types and the argument list.
public class MathUtil
{
public int Sum(int a, int b) => a + b;
public double Sum(double a, double b) => a + b;
public int Sum(int a, int b, int c) => a + b + c;
}
// Callsites resolved at compile time
var u = new MathUtil();
var x = u.Sum(1, 2); // calls Sum(int,int)
var y = u.Sum(1.0, 2.0); // calls Sum(double,double)Method overriding (runtime)
Overriding lets a derived class provide a new implementation for a method declared virtual (or abstract) in a base class. At runtime, the CLR dispatches the call to the most-derived override (virtual dispatch).
public class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof");
}
Animal a = new Dog();
a.Speak(); // prints "Woof" — runtime chooses Dog.Speak()Side-by-side comparison
| Aspect | Overloading | Overriding |
|---|---|---|
| When resolved | Compile-time | Runtime (virtual dispatch) |
| Requirement | Different parameter signatures in same type | Derived method must match signature and use override for a virtual/abstract base |
| Goal | Provide multiple ways to call similar operation | Provide specialized behavior for subtypes |
| Polymorphism type | Static (ad-hoc) | Dynamic (subtype) |
Important nuances
- new vs override: If a derived class defines a method with the same signature without
override, it hides the base method (the compiler will warn if you omitnew). Calls through a base reference will use the base implementation (not polymorphic). - Interfaces: Polymorphism commonly uses interfaces — a variable typed as an interface can reference any implementation and calls are dispatched to the concrete implementation.
- Performance: Virtual dispatch has a small runtime cost compared to a direct call, but is usually negligible compared to its benefits for design clarity.
When to use which
- Use overloading to provide convenient variations of a method (different parameter sets), but keep overloads semantically consistent to avoid confusion.
- Use overriding to implement subtype-specific behavior when you expect callers to work with the base type but get derived behavior at runtime.
Practical tip
Favor clear contracts: when overriding, document base-class virtual behavior and invariants. Prefer interfaces for decoupling and use abstract/virtual members when base classes should provide shared implementation alongside extension points.
17 What are abstract classes and interfaces — when to use which?
What are abstract classes and interfaces — when to use which?
Definitions
Abstract class: A class that cannot be instantiated directly and may contain abstract members (no implementation) as well as concrete members (with implementation). It models an "is-a" relationship and can hold protected state, constructors, and helper methods.
Interface: A contract that defines a set of members (methods, properties, events, indexers) that implementing types must provide. Since C# 8, interfaces can also contain default implementations, static members, and private helpers (narrowing some traditional differences).
Key differences at a glance
| Feature | Abstract Class | Interface |
|---|---|---|
| Can contain fields | Yes (state) | No (no instance fields) |
| Constructors | Yes | No |
| Multiple inheritance | No (single base class) | Yes (implement many interfaces) |
| Default implementation | Yes | Possible since C# 8 (default methods) |
| Use for | Shared base behavior/state | Cross-cutting contracts and loose coupling |
When to use an abstract class
- When types are closely related and share common code, protected members, or fields.
- When you want to provide non-public helper methods or default behavior that derived classes can reuse.
- When you need a constructor or want to guarantee some initialization logic.
When to use an interface
- When you need to define a capability that many unrelated types may implement (e.g.,
IComparableIDisposable). - When you want to allow an implementing type to have other base classes — interfaces support multiple implementation.
- When you want a clean, decoupled contract for dependency injection, mocking and testing.
Examples
public abstract class RepositoryBase
{
protected readonly DbConnection _conn;
protected RepositoryBase(DbConnection conn) => _conn = conn;
public abstract T Get<T>(int id);
protected void Log(string m) { /* shared helper */ }
}
public interface IRepository
{
T Get(int id);
void Add(T item);
}
public class SqlRepository : RepositoryBase, IRepository<User>
{
public SqlRepository(DbConnection c) : base(c) { }
public override User Get(int id) { /* uses _conn and Log */ }
public void Add(User u) { /* implement */ }
}
Modern interface capabilities (C# 8+)
Interfaces may now provide default implementations and even private methods. This makes evolving large API surfaces easier but does not remove the primary design distinction: interfaces are for contracts and abstraction, abstract classes are for shared implementation/state.
Design guidance
- Prefer interfaces for public-facing contracts (APIs) and for types you will mock or substitute in tests.
- Use abstract classes when you have a clear, tight family of types that share implementation and internal state.
- Favor composition over inheritance when sharing behavior across unrelated types to reduce coupling.
18 What is the base keyword and where is it used?
What is the base keyword and where is it used?
Purpose of base
The base keyword lets a derived class access members of its direct base class. It's commonly used to invoke a specific base class constructor, call a base method implementation, or access hidden members.
Common uses
- Call a base constructor — initialize base class state from a derived class constructor.
- Call a base method — when overriding, you may still want to use the base behavior as part of the override.
- Access hidden members — if a derived member hides a base member (
new),basecan access the original.
Examples
public class Person
{
protected string name;
public Person(string name) => this.name = name;
public virtual void Introduce() => Console.WriteLine($"I am {name}");
}
public class Employee : Person
{
public int Id { get; }
public Employee(string name, int id) : base(name) // call base constructor
{
Id = id;
}
public override void Introduce()
{
base.Introduce(); // call base implementation
Console.WriteLine($"My employee id is {Id}");
}
}
// Usage
var e = new Employee("Alice", 42);
e.Introduce();
Hidden members / new
public class A { public void Foo() => Console.WriteLine("A.Foo"); }
public class B : A { public new void Foo() => Console.WriteLine("B.Foo");
public void CallBase() => base.Foo(); }
var b = new B();
b.Foo(); // calls B.Foo
b.CallBase(); // calls A.Foo via base.Foo()base vs this
this refers to the current instance, while base explicitly targets the base-class view of that instance. You cannot use base to access non-inherited types and it only works inside instance methods or constructors.
Limitations
baseonly refers to the direct base class, not arbitrary ancestor types.- It cannot be used in static members because base relates to instance inheritance.
Design tips
Use base when you need to combine base-class behavior with derived behavior or explicitly call a base constructor. Avoid overusing hidden members (new) as they break polymorphism and often indicate design issues.
19 How do properties (getters/setters) work in C#?
How do properties (getters/setters) work in C#?
What is a property?
A property in C# provides a controlled way to expose the data of an object using get and set accessors. Under the hood properties are methods (accessors), but the syntax looks like field access which improves encapsulation and readability.
Basic property syntax
public class Person
{
private string _name; // backing field
public string Name // property
{
get { return _name; }
set { _name = value; }
}
}
// Usage
var p = new Person();
p.Name = "Alice"; // calls set
Console.WriteLine(p.Name); // calls getAuto-properties (convenience)
Auto-properties let the compiler generate a hidden backing field automatically, reducing boilerplate:
public class Person
{
public string Name { get; set; } // compiler creates backing field
}
// Read-only auto-property (init-only for initialization)
public class Config
{
public string Host { get; init; }
}
var c = new Config { Host = "localhost" }; // allowed during object init
Expression-bodied and computed properties
public double Radius { get; set; }
public double Area => Math.PI * Radius * Radius; // read-only computed propertyAccess modifiers on accessors
You can restrict accessibility on individual accessors:
public string Secret { get; private set; }
// public getter, private setter
Property change notifications
In UI scenarios you often implement INotifyPropertyChanged so changes to properties raise events. This requires a custom setter to raise the notification:
public class ViewModel : INotifyPropertyChanged
{
private string _title;
public string Title
{
get => _title;
set
{
if (_title != value)
{
_title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Backing fields vs computed
Use backing fields when you need storage or custom logic (validation, raising events). Use computed properties when the value is derived from other state and does not need storage.
Best practices
- Prefer auto-properties for simple data carriers (DTOs, models).
- Perform validation or side-effects in the setter carefully — try to keep setters simple and predictable.
- Prefer
initfor immutable objects that should only be set during object initialization. - Remember to implement
GetHashCodeandEqualsappropriately when properties participate in equality semantics.
20 What are indexers and how do you implement one?
What are indexers and how do you implement one?
What is an indexer?
An indexer allows instances of a class or struct to be indexed using array-like syntax (instance[index]). It is declared with the this keyword and one or more parameters. Indexers make custom collection classes feel natural to use.
Basic indexer syntax
public class WeekDays
{
private string[] names = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
public string this[int index]
{
get => names[index];
set => names[index] = value;
}
}
var w = new WeekDays();
Console.WriteLine(w[1]); // "Mon"
w[1] = "Monday"; // set via indexerIndexers with different parameter types
You can overload indexers by parameter type/number, although typical classes provide a single indexer. Dictionaries often provide an indexer by key type:
public class MyMap
{
private Dictionary<string,int> _d = new();
public int this[string key]
{
get => _d[key];
set => _d[key] = value;
}
}
var m = new MyMap();
m["a"] = 10;
int x = m["a"];Read-only indexer
public string this[int i] => names[i]; // read-onlyUse cases
- Wrap internal collections and expose convenient indexing semantics.
- Implement custom lookup logic, computed indices, or caching semantics behind a friendly API.
- Expose multi-dimensional indexing (e.g.,
this[int row, int col]).
Under the hood & metadata
Indexers are implemented as properties named "Item" in metadata. When interoperating with other CLI languages, indexers appear as special properties rather than a language-only feature.
Best practices & pitfalls
- Indexers should be intuitive: keep indexing behavior predictable and document bounds/exception semantics (IndexOutOfRangeException / KeyNotFoundException).
- Avoid expensive operations inside indexers — indexers are expected to be O(1) or reasonable; expensive work can surprise callers.
- Prefer explicit methods (e.g.,
TryGetValue) for operations that may fail instead of throwing exceptions from indexers.
Example — custom matrix with 2D indexer
public class Matrix
{
private readonly double[,] data;
public int Rows { get; }
public int Cols { get; }
public Matrix(int r, int c)
{
Rows = r; Cols = c; data = new double[r, c];
}
public double this[int row, int col]
{
get => data[row, col];
set => data[row, col] = value;
}
}
var A = new Matrix(3,3);
A[0,1] = 2.5;
Console.WriteLine(A[0,1]);
21 What are delegates and how do they differ from events?
What are delegates and how do they differ from events?
What is a delegate?
A delegate is a type that represents references to methods with a specific signature and return type. Delegates are type-safe — you can only assign methods that match the delegate's signature. They behave like function pointers but are fully managed by the CLR and support multicast invocation.
Why delegates?
- Pass methods as parameters.
- Create callback APIs.
- Support multicast invocation (invoke multiple handlers in order).
Basic delegate declaration and use
public delegate void Notify(string message);
public class Broadcaster
{
public Notify OnNotify; // public delegate field (not recommended for public APIs)
public void Raise(string m) => OnNotify?.Invoke(m);
}
void Handler(string msg) => Console.WriteLine("Got: " + msg);
var b = new Broadcaster();
b.OnNotify += Handler; // subscribe
b.Raise("Hello");
What is an event?
An event is a language-level construct built on delegates that restricts how the delegate can be used by external code: clients can only subscribe (+=) or unsubscribe (-=), but cannot invoke/reset the delegate directly. This encapsulation prevents external code from raising the event or replacing the invocation list.
Event example
public class Broadcaster
{
public event Notify OnNotify; // event encapsulating the delegate
protected virtual void Raise(string m) => OnNotify?.Invoke(m);
public void DoSomething() { Raise("Hi"); }
}
// Usage
var b = new Broadcaster();
b.OnNotify += Handler; // OK
// b.OnNotify = null; // compile error — cannot assign outside
Delegate vs Event — quick comparison
| Aspect | Delegate | Event |
|---|---|---|
| Direct invocation by outsiders | Allowed (if delegate field is public) | Not allowed — only the declaring class can raise |
| Subscription model | Yes (multicast) | Yes (multicast with encapsulation) |
| Best practice | Use privately or for internal plumbing | Expose events for public observer/notification APIs |
Multicast behavior
Delegates can reference multiple methods (multicast). When invoked, they call each method in the invocation list in order. If the delegate has a return type, only the return value of the last invoked handler is returned (hence events for return values are rarely used).
Recommended patterns
- Expose notifications as
eventrather than raw delegate fields to protect invocation control. - Use the standard event pattern
event EventHandler<TEventArgs>orEventHandlerand create anOnXxxprotected virtual method to raise the event. - For modern APIs, consider
Action<...>orFunc<...>delegates for private callbacks but keep public APIs event-based.
22 What are events and how are they used with delegates?
What are events and how are they used with delegates?
Event fundamentals
An event is a named notification mechanism. It allows a publisher to provide a notification endpoint to which multiple subscribers can attach handlers. Events are implemented using delegates under the hood and provide encapsulation so only the publisher can raise them.
Typical event pattern (recommended)
Use EventHandler or EventHandler<TEventArgs> for typed events. Implement a protected OnXxx method to raise the event to allow derived classes to override raising behavior.
public class ProgressChangedEventArgs : EventArgs
{
public int Percent { get; }
public ProgressChangedEventArgs(int p) => Percent = p;
}
public class Worker
{
public event EventHandler<ProgressChangedEventArgs> ProgressChanged;
protected virtual void OnProgressChanged(int p)
{
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(p));
}
public void DoWork()
{
for (int i = 0; i <= 100; i += 10)
{
// do work...
OnProgressChanged(i);
}
}
}
// Subscriber
var w = new Worker();
w.ProgressChanged += (s, e) => Console.WriteLine(e.Percent);
w.DoWork();
Thread-safety considerations
To avoid race conditions when raising events, read the event delegate into a local variable first (older guidance). With null-propagation operator (?.) it is usually safe, but if handlers can be added/removed concurrently in multithreaded scenarios, consider using synchronization or immutable invocation lists.
// defensive read (older pattern)
var handler = ProgressChanged;
if (handler != null) handler(this, args);
// modern concise pattern
ProgressChanged?.Invoke(this, args);
Unsubscribing and memory leaks
Subscribers should unsubscribe from events when no longer interested, especially if the publisher outlives the subscriber (common source of memory leaks). Patterns to alleviate this include weak events (WeakReference) or explicit unsubscription in disposal.
Event variants
- Standard
EventHandler/EventHandler<T>. - Custom delegates for special signatures (rare nowadays).
- Observable patterns (IObservable/IObserver) for reactive scenarios.
When to use events
Use events for loosely-coupled notifications (UI updates, progress, lifecycle events). For more advanced reactive scenarios, prefer Rx/IObservable patterns which provide richer composition and cancellation semantics.
23 What are lambda expressions and expression trees; when would you use them?
What are lambda expressions and expression trees; when would you use them?
Lambda expressions
A lambda expression is an anonymous function you can use to create delegates or expression trees. Syntax: (parameters) => expression or (parameters) => { statements }. They make code concise and are heavily used with LINQ and functional-style APIs.
// examples
Func<int,int,int> add = (x, y) => x + y;
var nums = new[] {1,2,3,4};
var evens = nums.Where(n => n % 2 == 0); // LINQ with lambda
Expression trees
An expression tree represents code as a data structure (System.Linq.Expressions.Expression<T>). Instead of compiling the lambda to IL directly, the compiler builds an object graph that describes the operations. Expression trees enable runtime analysis, modification, and translation of code (e.g., converting LINQ expressions to SQL).
Expression<Func<int,int,bool>> expr = x => x > 5;
// inspect expr.Body, expr.Parameters, etc.
When to use which
- Use lambda expressions to pass small functions, callbacks, or predicates to methods (LINQ, async callbacks).
- Use expression trees when you need to examine or translate the code: example scenarios — ORMs (Entity Framework) converting expressions to SQL, dynamic query builders, or building rule engines.
Performance notes
Lambdas compiled to delegates are fast; building expression trees has overhead and compiling them to delegates is more expensive. Use expression trees only when their introspectability is required.
Example — LINQ provider difference
// IQueryable<T> provider receives expression tree and can translate to SQL
IQueryable<User> q = dbContext.Users;
var admins = q.Where(u => u.IsAdmin); // expression tree visited by EF to produce SQL
// IEnumerable<T> uses compiled delegates and LINQ-to-Objects executes in-memory
IEnumerable<User> list = someList;
var filtered = list.Where(u => u.IsAdmin); // predicate executed in CLR
24 Explain extension methods and give a practical use case.
Explain extension methods and give a practical use case.
What are extension methods?
Extension methods let you "attach" new methods to existing types without modifying their source or creating derived types. They are static methods in static classes with the first parameter decorated with this to indicate the extended type.
public static class StringExtensions
{
public static bool IsNullOrEmptyEx(this string s) => string.IsNullOrEmpty(s);
}
// Usage
string s = null;
if (s.IsNullOrEmptyEx()) { /* ... */ }
Common use cases
- Utility helpers for framework types (strings, collections), e.g.,
string.IsNullOrWhiteSpace. - Fluent APIs for configuration or builders.
- Add domain-specific behavior to third-party types in a clean, discoverable way.
Resolution rules
Extension methods are considered only if no instance method with a matching signature exists. They require a using directive to bring the static class' namespace into scope.
Design considerations
- Use extension methods for behavior that feels natural as an instance method and is broadly useful.
- Avoid extension methods that obscure important differences or change semantics in surprising ways.
- Prefer naming extension methods clearly and place them in logical namespaces to avoid collisions.
Advanced example — LINQ-like extensions
public static class EnumerableExtensions
{
public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> src, int step)
{
if (step <= 0) throw new ArgumentOutOfRangeException(nameof(step));
int i = 0;
foreach (var item in src)
{
if (i++ % step == 0) yield return item;
}
}
}
var data = Enumerable.Range(1, 10).TakeEvery(3); // 1,4,7,10
25 What are generics and how do they provide type safety?
What are generics and how do they provide type safety?
What are generics?
Generics allow you to define classes, interfaces, and methods with a placeholder for the type they operate on. Instead of using object and casting, generics let the compiler enforce type correctness and avoid runtime casts or boxing.
public class Box<T>
{
public T Value { get; set; }
}
var intBox = new Box<int> { Value = 42 };
var strBox = new Box<string> { Value = "hi" };
Benefits
- Type safety at compile time (no invalid casts).
- Avoids boxing for value types (better performance).
- Reusability — one implementation works across many types.
Generic constraints
You can constrain type parameters using where clauses (e.g., where T : classwhere T : new()where T : BaseType). This allows calling members or constructing T safely.
public class Factory<T> where T : new()
{
public T Create() => new T();
}
Variance
Interfaces and delegates can be covariant (out) or contravariant (in) for reference types, enabling safe assignment conversions (e.g., IEnumerable<string> can be used where IEnumerable<object> is expected).
Example — generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
var m = Max(3, 5); // T inferred as int
When to use generics
Whenever an algorithm or container is type-agnostic and you want compile-time guarantees and no boxing. Collections, caching, factories, and utility algorithms are primary candidates.
26 What is pattern matching and how has it evolved in recent C# versions?
What is pattern matching and how has it evolved in recent C# versions?
What is pattern matching?
Pattern matching is a set of language features that allow you to match a value against a pattern and deconstruct or test properties in a concise way. It replaces verbose type checks and casts with readable expressions.
Core forms
isexpression pattern:if (obj is string s) …- Switch expressions and switch statements with patterns.
- Property patterns: match based on member values.
- Positional/tuple patterns: deconstruct objects in patterns.
Examples
// is-pattern
if (o is Person p && p.Age >= 18) Console.WriteLine(p.Name);
// switch expression
string Describe(object o) => o switch
{
int i => $"int {i}"
string s => $"string {s}"
Person { Age: >= 18 } adult => $"Adult {adult.Name}"
_ => "unknown"
};
// positional pattern (with Deconstruct)
public class Point { public int X; public int Y; public void Deconstruct(out int x, out int y) { x = X; y = Y; } }
var p = new Point { X = 1, Y = 2 };
if (p is (1, 2)) Console.WriteLine("origin offset");
Evolution
Pattern matching started with simple is checks, then expanded to switch patterns, property and tuple/positional patterns, relational patterns (<, >, etc.), logical patterns (andornot), and recursive patterns that allow expressive matching of nested structures.
Benefits
- Less boilerplate and safer code (fewer casts).
- More expressive branching (combine multiple conditions concisely).
- Works well with discriminated-union-like designs using records and deconstruction.
When to use
Use pattern matching for complex conditional logic, parsing heterogeneous input, or when you want concise deconstruction in switch-like flows. It improves readability and reduces error-prone casting logic.
27 Explain tuples in C# and their use cases.
Explain tuples in C# and their use cases.
What are tuples?
Tuples in modern C# (ValueTuple) are lightweight value-type containers for grouping multiple values. They support element names and deconstruction, making them handy for returning multiple values without defining a separate type.
// simple tuple
(string Name, int Age) GetPerson() => ("Alice", 30);
var t = GetPerson();
Console.WriteLine(t.Name); // "Alice"
// deconstruction
var (name, age) = GetPerson();
Advantages
- No need for small DTO classes for throwaway return values.
- Named elements improve readability (compared to Item1/Item2).
- Being a value type, tuples avoid extra heap allocation in many cases.
Considerations
- For public APIs or complex data structures, prefer explicit types (classes/records) for clarity and versioning.
- Tuples are best for local scope, internal helpers, or private methods where performance and conciseness matter.
Records vs tuples
Records (C# 9+) are a structured way to declare immutable data types with named properties and better semantics for equality, documentation, and evolution. Use records for domain models; use tuples for quick, local aggregations.
public record PersonRecord(string Name, int Age);
// vs tuple (string Name, int Age)
28 What are anonymous types and their typical usage?
What are anonymous types and their typical usage?
What are anonymous types?
Anonymous types provide a quick way to create objects with read-only properties without declaring a new class. The compiler generates a sealed type with properties and uses structural equality for comparisons.
var anon = new { Name = "Bob", Age = 25 };
Console.WriteLine(anon.Name); // Bob
// anon.Name = "Alice"; // compile-time error — properties are init-only/read-only
Common scenarios
- Shaping results in LINQ queries:
select new { u.Name, u.Email }. - Temporary grouping of values when a dedicated DTO is unnecessary.
Limitations
- Anonymous types are local to the assembly and typically limited to method scope.
- They are immutable/read-only and not suitable for public API surface.
- Type inference uses
var; you cannot declare a method return type as an anonymous type.
Alternatives
For public-facing or reusable structures, define a class or a record. Records give similar conciseness with explicit semantics and are suitable for APIs.
29 How does the using statement and IDisposable pattern work?
How does the using statement and IDisposable pattern work?
IDisposable — purpose
IDisposable defines a single method, Dispose(), that types implement to release unmanaged resources (file handles, sockets, native memory) or other expensive resources deterministically.
using statement (classic)
using (var stream = File.OpenRead(path))
{
// use stream
} // stream.Dispose() is called automatically even if exceptions occur
using declaration (C# 8+)
using var stream = File.OpenRead(path);
// scope end: stream.Dispose() automatically called
Implementing Dispose pattern
For simple managed resource cleanup, implement IDisposable and release resources in Dispose(). If your type holds unmanaged resources directly or needs finalization, implement the full dispose pattern with a finalizer and suppression of finalization:
public class ResourceHolder : IDisposable
{
private IntPtr native = /* native handle */;
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
if (disposing)
{
// free managed resources
}
// free native resources
if (native != IntPtr.Zero) { /* free */ native = IntPtr.Zero; }
disposed = true;
}
~ResourceHolder() => Dispose(false);
}
Guidelines
- Prefer
usingfor short-lived resources to ensure deterministic cleanup. - Implement
IDisposablefor types that manage resources (streams, DB connections). Use finalizers only when necessary. - Call
GC.SuppressFinalize(this)after successful dispose to avoid extra finalization overhead.
30 What are attributes and how do you define/consume custom attributes?
What are attributes and how do you define/consume custom attributes?
What are attributes?
Attributes provide declarative metadata that you can attach to assemblies, types, methods, properties, parameters, and more. At runtime (or compile-time via analyzers) you can read attributes using reflection to customize behavior.
Defining a custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class DocumentationAttribute : Attribute
{
public string Description { get; }
public DocumentationAttribute(string description) => Description = description;
}
[Documentation("This class handles payments")]
public class PaymentProcessor { }
Reading attributes via reflection
var attr = (DocumentationAttribute)Attribute.GetCustomAttribute(typeof(PaymentProcessor), typeof(DocumentationAttribute));
Console.WriteLine(attr?.Description);
Common uses
- Configure serialization, mapping, or validation (e.g., JsonProperty, Required).
- Framework metadata (e.g., routing in web frameworks, authorization policies).
- Tooling and code-generation hints (e.g., unit test markers).
Best practices
- Keep attributes small and focused.
- Prefer constructor arguments for required information and properties for optional settings.
- Use
AttributeUsageto limit where the attribute applies and whether it can be inherited or repeated.
31 What are the main collection types in .NET and when to use them?
What are the main collection types in .NET and when to use them?
Overview
.NET provides a rich set of collection types in System.Collections and System.Collections.Generic. Choosing the right collection depends on the operations you need (indexing, iteration, insertion/removal patterns, uniqueness, ordering) and performance characteristics.
Common collections & purpose
| Type | When to use | Characteristics |
|---|---|---|
Array (T[]) | Fixed-size collections with fastest indexed access | Contiguous memory, O(1) index, fixed length |
List<T> | Dynamic array for most general-purpose needs | Amortized O(1) append, O(1) index, O(n) insert/remove middle |
LinkedList<T> | Frequent inserts/removes in the middle and stable node references | O(1) insert/remove given node, O(n) indexing, poor locality |
Dictionary<TKey,TValue> | Key → value lookup | Hash-based, O(1) average lookup/insert, requires stable key hash |
HashSet<T> | Maintain unique items and fast membership checks | Hash-based, O(1) average contains/add |
Queue<T> | FIFO processing | Enqueue/Dequeue O(1) |
Stack<T> | LIFO processing or recursion emulation | Push/Pop O(1) |
SortedList / SortedDictionary | Ordered maps by key | Tree-based, O(log n) operations, keeps keys sorted |
Practical examples
// List<T>
var list = new List<int>();
list.Add(1); // good general purpose
// Dictionary<TKey,TValue>
var dict = new Dictionary<string,int>();
dict["apple"] = 3;
if (dict.TryGetValue("apple", out var value)) Console.WriteLine(value);
// HashSet<T>
var set = new HashSet<int>();
set.Add(1); // ensures uniqueness
// Queue<T>
var q = new Queue<string>();
q.Enqueue("first");
var next = q.Dequeue();
// Stack<T>
var stack = new Stack<char>();
stack.Push('a');
var top = stack.Pop();
Guidelines & trade-offs
- Prefer
List<T>for dynamic arrays and most general scenarios. - Use
Dictionary<TKey,TValue>for fast key lookups — choose a good key type and consider customIEqualityComparerif necessary. - Use
HashSet<T>when membership/uniqueness matters; avoid duplicates cheaply. - Avoid
LinkedList<T>unless you truly need O(1) insert/remove at arbitrary positions or stable node references — modern CPUs favor arrays for locality. - Use sorted collections (
SortedDictionarySortedSet) when you need data kept in key order but accept O(log n) costs.
Performance tip
Always measure on realistic workloads. Collections have different memory and CPU trade-offs; caching behavior and allocation patterns can dominate small micro-optimizations.
32 When do you use List<T> vs LinkedList<T> vs array?
When do you use List<T> vs LinkedList<T> vs array?
Quick summary
All three represent ordered collections but with different performance and memory characteristics. Choose based on how often you need random access, insert/remove in the middle, and whether the size is known.
| Operation | Array | List<T> | LinkedList<T> |
|---|---|---|---|
| Indexing (get/set) | O(1) | O(1) | O(n) |
| Append | O(1) (if pre-sized) / O(n) if resizing manually | Amortized O(1) | O(1) if you have tail ref |
| Insert/remove middle | O(n) (shift elements) | O(n) (shift elements) | O(1) given node reference |
| Memory | Minimal overhead | Extra overhead for capacity vs count | High per-node overhead (next/prev pointers) |
When to use an array
- Fixed-size collections or when you can pre-allocate known capacity.
- Highest performance for numeric or tight loops due to contiguous memory and CPU cache friendliness.
- Interop with APIs that expect arrays.
When to use List<T>
- Most general use-case when you need a resizable collection.
- You need O(1) random access and amortized O(1) append.
- Prefer it unless you have a clear reason to use a linked list.
When to use LinkedList<T>
- When you frequently insert/remove elements given a node reference (e.g., move nodes around) — operations are O(1).
- When you need stable references to nodes while mutating the list.
- Avoid it for heavy random access or large memory footprint concerns.
Examples
// Array - good for fixed buffers
int[] buffer = new int[1024];
// List - general purpose
var customers = new List();
customers.Add(new Customer());
// LinkedList - moving nodes efficiently
var list = new LinkedList();
var node = list.AddLast(1);
list.AddAfter(node, 2); // O(1) with node
Rule of thumb
Start with List<T> for general needs. Use arrays for performance-critical, pre-sized scenarios. Use LinkedList<T> only when you need its particular O(1) node insert/remove behavior or stable node references — these scenarios are less common.
33 How does Dictionary differ from Hashtable and when to use each?
How does Dictionary differ from Hashtable and when to use each?
Historical context
Hashtable is the older, non-generic hash-based map introduced in early .NET. Dictionary<TKey,TValue> (generics) was introduced later and is the recommended type for new code because it is type-safe and more efficient.
Key differences
| Aspect | Hashtable | Dictionary<TKey,TValue> |
|---|---|---|
| Generic | No (stores object) | Yes — type parameters for key and value |
| Type safety | No — requires boxing/unboxing for value types and casts | Yes — compile-time safety, no casts |
| Performance | Less efficient due to boxing/casts for value types | Better performance and fewer allocations |
| API modernity | Older API | Modern, rich API (TryGetValue, etc.) |
Example — Dictionary usage
var dict = new Dictionary<string,int>();
dict["apple"] = 5;
if (dict.TryGetValue("apple", out var count)) Console.WriteLine(count);
// Hashtable example (legacy)
var ht = new Hashtable();
ht["apple"] = 5;
int c = (int)ht["apple"]; // requires cast
When to use which
- Prefer
Dictionary<TKey,TValue>for new code — it's type-safe and efficient. - Only use
Hashtablewhen working with very old code or APIs that require it (rare).
Best practices for Dictionary
- Use
TryGetValueto avoid throwing on missing keys. - Provide an initial capacity when you can to avoid rehashing overhead (
new Dictionary<TKey,TValue>(capacity)). - Consider a custom
IEqualityComparer<TKey>for specialized hashing or case-insensitive keys.
34 What is IEnumerable vs IQueryable and where should each be used?
What is IEnumerable vs IQueryable and where should each be used?
High-level difference
IEnumerable<T> is the basic interface for in-memory iteration, while IQueryable<T> extends it with expression-tree capabilities so query providers (like EF Core) can translate queries to remote execution engines (SQL, OData).
Behavior & execution
| Aspect | IEnumerable<T> | IQueryable<T> |
|---|---|---|
| Execution model | Deferred execution, runs in-memory (LINQ-to-Objects) | Deferred execution, query provider translates Expression tree to remote execution |
| Use case | In-memory collections, in-process filtering | Database/remote providers that translate queries to SQL or other languages |
| Where clauses | Executed in CLR after enumeration | Translated into provider-specific expression (e.g., SQL WHERE) |
Example — difference in practice
// IQueryable from EF DbSet: translated to SQL
IQueryable<User> q = dbContext.Users.Where(u => u.IsActive);
// The above builds an expression tree. SQL is generated when materialized (ToList, FirstOrDefault, etc.)
// IEnumerable: executed in memory
IEnumerable<User> list = someList.Where(u => u.IsActive); // filter done in CLR
When to use each
- IEnumerable<T>: when data is already in memory or you want LINQ-to-Objects semantics.
- IQueryable<T>: when working with a provider (EF, NHibernate, OData) that can translate the expression tree to an efficient remote query — it moves filtering/sorting to the source.
Practical tips
- Prefer forming as much of the query as possible against
IQueryableso expensive filters happen in the database. - Be careful when mixing
IQueryableandIEnumerable— calling a method that forces enumeration (e.g.,AsEnumerable()ToList()) may prematurely bring data into memory. - When building provider-agnostic helpers, accept
IEnumerable<T>unless you explicitly need provider translation.
35 Basics of LINQ and advantages of using it.
Basics of LINQ and advantages of using it.
What is LINQ?
LINQ (Language-Integrated Query) is a set of language features and APIs in .NET that provide a consistent, declarative way to query data from different sources — in-memory collections, XML, databases, and more — using a common syntax and operators.
Core concepts
- Query operators:
SelectWhereGroupByJoinOrderByAggregate, etc. - Deferred execution: Queries are not executed until materialized (
ToListFirst, enumeration). - Composable: Operators can be chained to form complex queries.
- Provider model:
IQueryableproviders can translate expression trees to other query languages (e.g., SQL).
Examples — query & method syntax
// Method syntax
var adults = people.Where(p => p.Age >= 18).OrderBy(p => p.Name).Select(p => p.Name);
// Query syntax (similar to SQL)
var q = from p in people
where p.Age >= 18
orderby p.Name
select p.Name;
Advantages
- Concise, expressive queries that reduce boilerplate iteration code.
- Type-safe queries with compile-time checking.
- Provider-agnostic programming model — same query style works for collections or databases.
- Readable and maintainable transformations for collection pipelines.
When not to overuse LINQ
For extremely performance-sensitive inner loops or when LINQ hides expensive operations (e.g., repeated deferred queries), prefer explicit loops or optimize the query (materialize once, avoid nested enumeration).
36 How do you sort/filter/aggregate collections with LINQ?
How do you sort/filter/aggregate collections with LINQ?
Common LINQ operators
Sorting: OrderByOrderByDescendingThenBy. Filtering: Where. Projection: Select. Grouping: GroupBy. Aggregation: SumAverageCountAggregate.
Examples
var topNames = people
.Where(p => p.Age >= 18) // filter
.OrderByDescending(p => p.Score) // sort
.ThenBy(p => p.Name)
.Select(p => p.Name) // project
.Take(10);
var total = orders.Where(o => o.Date >= start).Sum(o => o.Amount);
var grouped = people.GroupBy(p => p.Country)
.Select(g => new { Country = g.Key, Count = g.Count(), AvgAge = g.Average(x => x.Age) });
Query syntax equivalent
var q = from p in people
where p.Age >= 18
orderby p.Score descending, p.Name
select p.Name;
Aggregation details
Use Aggregate for custom reductions. Use GroupBy to partition data before aggregation. Prefer built-in aggregators (SumAverageMinMaxCount) where possible for clarity and performance.
Performance tips
- Materialize queries (e.g.,
ToList) when you will enumerate multiple times to avoid recomputation. - Avoid calling expensive methods inside selectors or predicates repeatedly.
- For database-backed queries, compose as much as possible on the
IQueryableside so filters and aggregates execute in the database.
37 How do you handle large data sets efficiently (streaming, pagination)?
How do you handle large data sets efficiently (streaming, pagination)?
Core strategies
- Streaming / lazy enumeration: Use
IEnumerable<T>withyield returnor data readers to process items one-by-one without loading all data into memory. - Pagination: Query and fetch only a page of items at a time (skip/take at DB level), suitable for UIs and REST APIs.
- Batch processing: Chunk data into manageable batches for processing, committing, or sending over the network.
- Push-based streaming: Use pipelines (e.g., IAsyncEnumerable, streams, reactive libraries) for asynchronous streaming workflows.
Examples
// streaming from an iterator
IEnumerable<Record> ReadLargeFile(string path)
{
using var r = File.OpenText(path);
string line;
while ((line = r.ReadLine()) != null)
{
yield return Parse(line);
}
}
// pagination in EF/Core
var page = await db.Users.OrderBy(u => u.Id).Skip((pageNo-1)*pageSize).Take(pageSize).ToListAsync();
// async streaming with IAsyncEnumerable (C# 8+)
async IAsyncEnumerable<T> StreamRemoteAsync()
{
await foreach (var item in client.GetStreamAsync())
yield return item;
}
Offload work to the source
For very large datasets backed by a DB, always push filtering, sorting, grouping and paging to the database (via IQueryable and provider translation) rather than materializing everything into memory and then operating on it.
Memory and allocation control
- Use streaming APIs and
Span<T>/Memory<T>where possible to minimize allocations. - Rent buffers from
ArrayPool<T>for repeated IO operations.
Monitoring & back-pressure
When producing and consuming streams at different rates, consider back-pressure mechanisms (ReactiveX, Channels) or flow-control to avoid OOM or resource thrash.
38 Explain yield return and iterator blocks.
Explain yield return and iterator blocks.
What does yield return do?
The yield return statement simplifies creating enumerators. When a method uses yield return, the compiler transforms it into a state machine that implements IEnumerable/IEnumerable<T> and yields values lazily as the caller iterates.
Benefits
- Memory efficient — produce elements on demand rather than allocating a full collection.
- Simpler code for complex iteration logic — no need to manually implement
IEnumerableorIEnumerator.
Example
IEnumerable<int> GetPrimes(int max)
{
for (int n = 2; n <= max; n++)
{
if (IsPrime(n)) yield return n;
}
}
foreach (var p in GetPrimes(1000)) Console.WriteLine(p); // primes generated as needed
Yield return vs building a list
Use yield return when immediate consumption or streaming is expected. If you need random access, multiple enumerations without re-running the generator, or snapshots, materialize into a collection (ToList()).
Yield break
yield break; exits the enumerator early and ends iteration.
IEnumerable<int> FirstN(int max, int n)
{
int i = 0;
while (i < max)
{
if (i++ > n) yield break;
yield return i;
}
}
Threading & state
Iterator state machines are not thread-safe. Each enumeration returns a fresh enumerator instance and its own state machine. Avoid sharing enumerators across threads without synchronization.
39 Explain async/await and how they simplify asynchronous code.
Explain async/await and how they simplify asynchronous code.
Concept
async and await are language features that let you write asynchronous code that looks synchronous. An async method returns a Task (or Task<T> / ValueTask) and can await other tasks; await yields control to the caller until the awaited task completes.
Why they help
- Linear, readable code instead of nested callbacks.
- Automatic capture and restoration of execution context (by default), which preserves synchronization context for UI apps.
- Composability of asynchronous operations using familiar constructs (try/catch, loops).
Example
public async Task<string> DownloadAsync(string url)
{
using var http = new HttpClient();
var data = await http.GetStringAsync(url); // asynchronous I/O
return data;
}
// caller
var content = await DownloadAsync("https://example.com");
Important details
- async does not make CPU-bound work faster: use
Task.Runfor offloading CPU work to background threads if needed. - Exception handling: exceptions thrown in async methods propagate via the returned
Taskand can be observed withawaitortask.ContinueWith. - Context capture:
awaitby default captures the current synchronization context and resumes on it. UseConfigureAwait(false)in library code to avoid capturing UI or request contexts and improve performance.
Best practices
- Use async all the way: avoid mixing synchronous blocking calls (e.g.,
.Result.Wait()) with async to prevent deadlocks. - Prefer
Task<T>overvoidfor async methods;async voidis only for event handlers. - Consider
ValueTaskwhen minimizing allocations for hot-path async methods that often complete synchronously.
40 Difference between Task, ValueTask, and Thread.
Difference between Task, ValueTask, and Thread.
Thread
A Thread is an OS-level scheduler entity that executes code independently. Creating threads is expensive (stack, OS resources) and switching between them is costly. Use threads for long-running, truly parallel CPU-bound work when you need dedicated execution contexts.
Task
Task (and Task<T>) is an abstraction representing an asynchronous operation. Tasks can represent CPU work scheduled on thread pool threads or I/O-bound operations that complete without occupying a thread. They are lightweight to compose and integrate with async/await.
ValueTask
ValueTask<T> is a value-type alternative to Task<T> designed to avoid heap allocations when an async method frequently completes synchronously. It can wrap either a Task<T> or a direct value. However, it has usage caveats (e.g., a ValueTask instance can only be awaited once unless you take care) so it's recommended mainly in high-performance library internals.
| Concept | Thread | Task | ValueTask |
|---|---|---|---|
| Nature | OS thread | Logical async operation | Value-type wrapper for async result |
| Creation cost | High | Relatively low (uses thread pool or I/O completion) | Lower allocations when sync completion occurs |
| Use with async | Manual threading | Primary building block for async/await | Optimization tool for hot paths |
When to use which
- Prefer
Taskand async/await for most asynchronous/I/O scenarios. - Use
Threaddirectly only when you need specialized thread behavior, thread-local stacks, or long-lived dedicated threads — otherwise use thread pool viaTask.Run. - Consider
ValueTaskfor performance-critical library methods that often produce synchronous results — measure carefully and follow the API constraints.
Example
// Task-based async
async Task<int> ComputeAsync()
{
await Task.Delay(100);
return 42;
}
// Thread example
var t = new Thread(() => DoWork());
t.Start();
// ValueTask (advanced optimization)
public ValueTask<int> MaybeCachedResultAsync()
{
if (_cachedAvailable) return new ValueTask<int>(_cachedValue); // no allocation
return new ValueTask<int>(ComputeAsync()); // wraps a Task
}
Final note
The Task-based model encourages composability and scales well for I/O-bound scenarios. Threads are lower-level primitives. ValueTask is a powerful optimization but adds complexity — use it only after profiling and understanding the trade-offs.
41 What is the Task Parallel Library (TPL) and when to use Parallel/PLINQ?
What is the Task Parallel Library (TPL) and when to use Parallel/PLINQ?
Overview
The Task Parallel Library (TPL) is a set of .NET libraries that make it easier to write parallel and concurrent code. The central abstraction is Task, which represents an asynchronous operation. TPL provides higher-level helpers like Task.RunTask.WhenAllParallel static APIs, and PLINQ (Parallel LINQ) to express parallelism without manually creating threads.
When to use what
- Use
Taskfor general asynchronous programming and when you need fine-grained control over asynchronous operations, composition (WhenAll/WhenAny), and coordination of IO-bound or mixed workloads. - Use
Parallel.For/ForEachfor CPU-bound data-parallel loops where each iteration is independent and can run on multiple cores. - Use PLINQ (
AsParallel()) to parallelize LINQ queries when transforming large in-memory collections in a data-parallel way.
Comparison table
| API | Good for | Notes |
|---|---|---|
Task | Asynchronous work, IO-bound or CPU-bound tasks with orchestration | Combinators (WhenAll/WhenAny), cancellation, continuations |
Parallel | CPU-bound loops (For, ForEach) | Automatic partitioning, uses thread pool threads |
PLINQ | Parallel LINQ queries over in-memory collections | Query-style, can use WithDegreeOfParallelism, beware of ordering/side-effects |
Example – Parallel.ForEach
var items = Enumerable.Range(1, 10000);
Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, item =>
{
// CPU-bound work per item
Process(item);
});Example – PLINQ
var results = data.AsParallel()
.WithDegreeOfParallelism(4)
.Where(x => ExpensivePredicate(x))
.Select(x => Transform(x))
.ToList();Practical cautions
- Parallelism is beneficial for CPU-bound workloads; for IO-bound tasks (
awaitable operations) prefer asynchronous Tasks to avoid blocking thread pool threads. - Avoid shared mutable state or protect it with proper synchronization; prefer immutable or partitioned data to minimize locking.
- Control parallelism degree (thread count) on servers to avoid oversubscription; use
MaxDegreeOfParallelismwhen needed. - PLINQ can change order and behavior of side-effecting operations — avoid side-effects in parallel queries.
42 How do you cancel asynchronous operations (CancellationToken)?
How do you cancel asynchronous operations (CancellationToken)?
Concept
C# uses the CancellationToken pattern to support cooperative cancellation. Producers of work accept a CancellationToken, and consumers (callers) request cancellation via a CancellationTokenSource. This design keeps control explicit and avoids aborting threads abruptly.
Typical usage
var cts = new CancellationTokenSource();
var token = cts.Token;
var task = Task.Run(async () =>
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
// do work
}
}, token);
// request cancellation from caller
cts.Cancel();
try { await task; }
catch (OperationCanceledException) { Console.WriteLine("Canceled"); }Key APIs
CancellationTokenSource.Cancel()— signal cancellation.token.ThrowIfCancellationRequested()— throwOperationCanceledException.- Many BCL APIs accept a
CancellationToken(e.g.,HttpClient.SendAsyncStream.ReadAsync).
Propagation
Pass the same token into child operations so cancellation propagates naturally. Use CancellationToken.None when a token is not required.
Timeouts
Cancellation can be combined with timeouts (cts.CancelAfter(TimeSpan)) or use Task.WhenAny to implement custom timeouts.
Best practices
- Make APIs accept tokens so callers can control lifetime.
- Do cooperative checks at appropriate points; avoid too-frequent checks that harm performance or too-rare checks that delay cancellation.
- Handle
OperationCanceledExceptionto perform cleanup and avoid treating cancellations as faults in logs.
43 How do you make asynchronous code testable and avoid deadlocks (ConfigureAwait)?
How do you make asynchronous code testable and avoid deadlocks (ConfigureAwait)?
Testability principles
To make async code testable: write tests that are async (support in test frameworks like xUnit, NUnit), avoid blocking synchronous waits on asynchronous tasks, mock asynchronous dependencies, and keep side-effects observable.
Example — async test
[Fact]
public async Task MyAsyncMethod_Completes()
{
var svc = new MyService();
var result = await svc.DoWorkAsync();
Assert.True(result);
}
Deadlock scenario
Deadlocks often occur when synchronous code blocks waiting for an async Task that needs to resume on the captured synchronization context (e.g., UI thread). Example:
// BAD: may deadlock on GUI or ASP.NET classic contexts
var result = myService.DoWorkAsync().Result; // blocks current thread
ConfigureAwait(false)
Calling await someTask.ConfigureAwait(false) tells the awaiter not to capture the current synchronization context; the continuation can run on any thread. Library code should generally use ConfigureAwait(false) to avoid depending on a context, while application/UI code may need the context for UI updates.
public async Task<T> GetAsync<T>(string url)
{
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonSerializer.Deserialize<T>(content);
}
Guidelines
- Use async test methods and await tasks — do not block with
.Resultor.Wait(). - In libraries, use
ConfigureAwait(false)for every awaited call to reduce risk of deadlocks and improve performance by avoiding context captures. - In application code (UI), avoid
ConfigureAwait(false)where the continuation must run on the UI thread, but limit the UI-bound portion to the minimal code necessary. - Mock asynchronous dependencies (return completed tasks) to keep unit tests deterministic and fast.
44 Explain lock, Monitor, Mutex, SemaphoreSlim and when to use each.
Explain lock, Monitor, Mutex, SemaphoreSlim and when to use each.
Overview
Synchronization primitives control concurrent access to shared resources. Choose the right primitive based on scope (in-process vs cross-process), usage pattern (mutual exclusion vs throttling), and whether async/await compatibility is needed.
lock (Monitor)
lock(obj) is the most common, lightweight way to ensure only one thread executes a critical section at a time within the same process:
private readonly object _sync = new object();
lock (_sync)
{
// critical section
}
Under the hood it uses Monitor.Enter/Exit. It is not async-friendly (blocks thread), but it's fast for short critical sections.
Mutex
Mutex can be used across processes (named mutex). It is heavier and slower than Monitor but necessary when multiple processes must coordinate.
using (var m = new Mutex(false, "Global\MyAppMutex"))
{
m.WaitOne();
try { /* critical */ }
finally { m.ReleaseMutex(); }
}
SemaphoreSlim
SemaphoreSlim limits the number of concurrent callers and supports async waits (WaitAsync), making it a good choice for throttling or async scenarios:
private readonly SemaphoreSlim _sem = new SemaphoreSlim(3); // allow 3 concurrent
await _sem.WaitAsync();
try { await DoWorkAsync(); }
finally { _sem.Release(); }
Other primitives
ReaderWriterLockSlim— allows multiple concurrent readers and exclusive writer; useful when reads dominate writes.Interlocked— atomic operations for simple numeric updates without locks (e.g., increment counters).
Decision guidance
| Need | Recommended primitive |
|---|---|
| Simple in-process mutual exclusion | lock (Monitor) |
| Cross-process synchronization | Mutex |
| Async-friendly throttling / limited concurrency | SemaphoreSlim (with WaitAsync) |
| Many readers, few writers | ReaderWriterLockSlim |
| Simple atomic counters | Interlocked |
Practical tips
- Keep critical sections small to reduce contention.
- Avoid locking on publicly accessible objects (
lock(this)) — prefer private readonly objects. - Prefer
SemaphoreSlimfor async scenarios instead of blocking primitives.
45 What is a deadlock and how can you avoid it?
What is a deadlock and how can you avoid it?
What is a deadlock?
A deadlock happens when a cycle of dependencies between threads prevents any of them from progressing. Example: Thread A holds Lock 1 and waits for Lock 2; Thread B holds Lock 2 and waits for Lock 1.
Common causes
- Acquiring multiple locks in different orders.
- Blocking on async tasks (calling
.Resultor.Wait()) that require continuation on the blocked context. - Resource starvation with insufficient threads.
Example (classic two-lock deadlock)
lock(a)
{
Thread.Sleep(10);
lock(b) { /* ... */ }
}
// elsewhere
lock(b)
{
lock(a) { /* ... */ }
}
Strategies to avoid deadlocks
- Consistent lock ordering: Always acquire multiple locks in the same global order across your codebase.
- Minimize lock scope: Keep critical sections as short as possible.
- Prefer non-blocking patterns: Use concurrent collections (ConcurrentDictionary, ConcurrentQueue) or lock-free algorithms where possible.
- Timeouts and try-lock patterns: Use
Monitor.TryEnterwith timeouts to recover or log when contention is extreme. - Avoid synchronous blocking of async code: Use async/await and ConfigureAwait(false) appropriately instead of
.Resultor.Wait().
Detection & debugging
Use debugger tools (thread dump, Visual Studio Parallel Stacks) and performance profilers to detect threads waiting on locks. Logging lock acquisition and release (with correlation IDs) helps reproduce and diagnose deadlocks.
46 How do you ensure thread safety for shared mutable state?
How do you ensure thread safety for shared mutable state?
Principles
Thread safety means that concurrent access to shared data does not result in data races or inconsistent state. Achieve it by protecting shared data, reducing shared state, or using concurrency-safe data structures.
Common techniques
- Mutexes/locks:
lock/Monitor to serialize access to critical sections. - Atomic operations:
Interlocked.IncrementCompareExchangefor small numeric updates without full locks. - Concurrent collections: Use
ConcurrentDictionaryConcurrentQueue, etc., which are designed for safe concurrent access. - Immutability: Use immutable objects so readers never see partial updates — update by swapping references to new immutable versions.
- Message passing: Actor-like patterns (e.g., mailbox) where a single thread/process owns mutable state and others send messages.
Examples
// Interlocked example
int count = 0;
Interlocked.Increment(ref count);
// Concurrent collection
var dict = new ConcurrentDictionary<string,int>();
dict.AddOrUpdate("key", 1, (k, old) => old + 1);
// Immutable swap
volatile MyState _state;
void UpdateState(Func<MyState,MyState> updater)
{
MyState original, updated;
do
{
original = _state;
updated = updater(original);
} while (Interlocked.CompareExchange(ref _state, updated, original) != original);
}
Design recommendations
- Favor immutable data and pure functions where possible — they simplify reasoning about concurrency.
- Encapsulate synchronization within a small module so the rest of the codebase uses a safe API.
- Prefer higher-level concurrency constructs (TPL Dataflow, Channels) rather than ad-hoc locking.
- Profile and test under concurrency (stress tests) — concurrency bugs are often non-deterministic.
47 How do you read/write files in C# (streams, StreamReader/Writer, File helpers)?
How do you read/write files in C# (streams, StreamReader/Writer, File helpers)?
Key APIs
System.IO provides several levels of abstraction for file IO:
FileandFileInfo— convenience helpers for reading/writing small files.FileStream— low-level stream for reading/writing bytes.StreamReader/StreamWriter— wrappers for text IO with encoding support.BinaryReader/BinaryWriter— reading/writing primitive binary data.
Examples
// Simple (synchronous)
File.WriteAllText("path.txt", "Hello");
var text = File.ReadAllText("path.txt");
// Stream-based (recommended for large files)
using (var fs = new FileStream("big.bin", FileMode.Open, FileAccess.Read))
using (var br = new BinaryReader(fs))
{
// read binary
}
// Async example
using (var fs = new FileStream("large.txt", FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true))
using (var sr = new StreamReader(fs))
{
var content = await sr.ReadToEndAsync();
}
Encoding & text
Always be explicit about encodings when reading/writing text (UTF-8, UTF-16) to avoid platform-dependent bugs.
Performance tips
- Use buffered streams and set appropriate buffer sizes for large files.
- Use async APIs (
ReadAsync/WriteAsync) for IO-bound scenarios to keep threads responsive. - Use
FileStreamwithuseAsync: truefor efficient async IO on Windows.
48 Explain serialization/deserialization for JSON and XML.
Explain serialization/deserialization for JSON and XML.
What is serialization?
Serialization is the process of converting an object graph to a storable or transmittable format (JSON, XML, binary). Deserialization reconstructs objects from that representation. It's used for persistence, messaging, and interop with web APIs.
JSON serializers
System.Text.Json— built-in, high-performance JSON serializer in .NET Core/modern .NET. Good defaults and fast, but fewer legacy features than Newtonsoft.Newtonsoft.Json (Json.NET)— mature library with many features (custom converters, flexible attributes) and widespread usage.
Basic JSON example (System.Text.Json)
public class Person { public string Name { get; set; } public int Age { get; set; } }
var p = new Person { Name = "Alice", Age = 30 };
var json = JsonSerializer.Serialize(p);
var p2 = JsonSerializer.Deserialize<Person>(json);
XML serializers
XmlSerializer serializes objects to XML and uses attributes like [XmlElement][XmlAttribute]. XML is verbose but useful where schema (XSD) or XML features are required.
var xs = new XmlSerializer(typeof(Person));
using (var sw = new StringWriter())
{
xs.Serialize(sw, p);
var xml = sw.ToString();
}
Considerations
- Beware circular references — JSON serializers may fail unless configured to handle references.
- Versioning: add optional properties, default values, and custom converters to maintain compatibility.
- Security: avoid deserializing untrusted data into types that trigger code execution (type name handling in Newtonsoft can be risky).
49 Difference between XML serialization and JSON serialization (and common libraries).
Difference between XML serialization and JSON serialization (and common libraries).
Format differences
| Aspect | XML | JSON |
|---|---|---|
| Verbosity | More verbose, tag-based | More compact, key/value pairs |
| Schema | Supports XSD, strong schema validation | No built-in schema standard (JSON Schema exists but less commonly enforced) |
| Attributes | Supports attributes and element text | No attributes; everything is a value or nested object |
| Use cases | Document formats, SOAP, config with schema | APIs, lightweight messaging, web clients |
Library choices
- JSON:
System.Text.Json(fast, built-in),Newtonsoft.Json(feature-rich, flexible) - XML:
XmlSerializerDataContractSerializer(for WCF and advanced scenarios)
When to choose which
- Choose JSON for modern web APIs, smaller payloads, and better JS interop.
- Choose XML when schema validation, namespaces, or legacy systems require it.
Practical tip
Prefer System.Text.Json for new JSON workloads for performance; use Newtonsoft when you need backward compatibility or features not yet in System.Text.Json (e.g., advanced converters, polymorphic deserialization patterns).
50 How do you work with streams and buffers efficiently for binary data?
How do you work with streams and buffers efficiently for binary data?
Key concepts
Efficient binary IO focuses on minimizing allocations, reducing copies, and using appropriate buffering. .NET offers Span<T>Memory<T>ArrayPool<T>, and stream primitives to help.
Techniques
- Use
BufferedStreamor set a buffer size onFileStreamto avoid small kernel calls. - Use
ArrayPool<byte>.Shared.Rent()to reuse byte[] buffers and return them to pool when done to reduce GC pressure. - Use
Span<byte>/Memory<byte>for stack or slice-friendly operations without allocations. - Prefer
ReadAsync/WriteAsyncfor non-blocking IO; for high-performance scenarios useFileStreamwithuseAsync: true.
Example — using ArrayPool and async IO
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(8192);
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, useAsync: true);
int read;
while ((read = await fs.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Process(buffer.AsSpan(0, read));
}
}
finally
{
pool.Return(buffer);
}
Zero-copy & Span
Use Span<T> to operate on slices without allocations. Many APIs in modern .NET accept Span/Memory to avoid copies.
Performance tips
- Avoid allocating per-iteration buffers — reuse or rent buffers.
- Batch small writes into larger buffers where possible.
- Profile and measure — IO bottlenecks are often disk/network bound, not CPU bound.
51 What is reflection and what are common uses and caveats?
What is reflection and what are common uses and caveats?
Reflection
Reflection allows inspecting assemblies, types, and members at runtime. You can dynamically create objects, call methods, and read or modify fields. It's often used in frameworks, dependency injection, serialization, and tooling.
Uses
- Dynamically creating objects (
Activator.CreateInstance). - Invoking methods or accessing properties at runtime (
MethodInfo.InvokePropertyInfo.GetValue). - Frameworks for ORM, DI, serialization, and testing.
Caveats
- Slower than direct code due to runtime metadata lookups.
- Less type-safe, errors may appear at runtime.
- Can break with refactoring, obfuscation, or security restrictions.
52 How do you inspect types, methods, and create instances at runtime using reflection?
How do you inspect types, methods, and create instances at runtime using reflection?
Inspecting Types and Members
Use System.Type to get metadata about classes. Use GetMethodsGetPropertiesGetFields to inspect members. Create instances with Activator.CreateInstance and invoke methods with MethodInfo.Invoke. Binding flags allow access to non-public members.
Example
var type = typeof(MyClass);
var method = type.GetMethod("DoWork");
var instance = Activator.CreateInstance(type);
method.Invoke(instance, null);
53 How are attributes used with reflection? Give an example.
How are attributes used with reflection? Give an example.
Attributes and Reflection
Attributes provide metadata about types and members. Reflection can read these attributes at runtime using GetCustomAttributes, enabling dynamic behavior like validation, serialization, or DI.
Example
[Obsolete("Use NewMethod instead")]
public void OldMethod() {}
var method = typeof(MyClass).GetMethod("OldMethod");
var attrs = method.GetCustomAttributes(false);
54 What are expression trees and when would you use them?
What are expression trees and when would you use them?
Expression Trees
Expression trees represent code as data structures (Expression<T>) rather than executing it immediately. This enables runtime analysis, modification, or translation into other forms (like SQL or remote queries).
Use Cases
- LINQ providers translating queries to SQL or NoSQL.
- Dynamic code generation and performance optimization.
55 Describe the stack vs heap in .NET memory model.
Describe the stack vs heap in .NET memory model.
Stack vs Heap
The stack stores value types and method call frames with fast allocation/deallocation. The heap stores reference types managed by the GC. Understanding allocation helps optimize memory and performance.
Example
- Value types (int, struct) are on the stack.
- Reference types (class) are on the heap.
56 How does the garbage collector work; generations and large object heap?
How does the garbage collector work; generations and large object heap?
Garbage Collector (GC)
.NET GC is generational: Gen0, Gen1, Gen2 for short-lived and long-lived objects. Large Object Heap (LOH) stores large objects. GC runs automatically but can be tuned with background, server, or workstation modes.
Key Points
- Minimizes application pause times.
- Reclaims memory efficiently using compaction and generations.
57 What are finalizers and the Dispose pattern — how to implement properly?
What are finalizers and the Dispose pattern — how to implement properly?
Finalizers and Dispose Pattern
Finalizers (~Class) run before GC reclaims objects but are non-deterministic. Implement IDisposable to release unmanaged resources deterministically and call GC.SuppressFinalize to avoid redundant finalization.
Example
class MyClass : IDisposable {
~MyClass() { Dispose(false); }
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
protected void Dispose(bool disposing) {
if (disposing) { /* release managed */ }
/* release unmanaged */
}
} 58 What causes memory leaks in .NET and how to detect/prevent them?
What causes memory leaks in .NET and how to detect/prevent them?
Memory Leaks in .NET
Leaking memory occurs when objects are unintentionally retained (e.g., event handlers, static caches, unmanaged resources). Use memory profilers, heap snapshots, and weak references to detect and prevent leaks. Always follow IDisposable pattern for unmanaged resources.
59 How do you force garbage collection and why you normally shouldn't?
How do you force garbage collection and why you normally shouldn't?
Forcing Garbage Collection
You can call GC.Collect() to force a collection, but this is normally discouraged. The GC is optimized for overall performance, and forcing collection can cause pauses and reduce throughput. Use only for diagnostics or testing.
60 How to profile a .NET application and analyze memory dumps?
How to profile a .NET application and analyze memory dumps?
Profiling and Memory Analysis
Use profilers like dotTrace, PerfView, or Visual Studio Diagnostic Tools to analyze CPU and memory usage. Capture memory dumps with dotnet-gcdump or Windows crash dumps to investigate retained objects, large heaps, and suspicious growth patterns.
Best Practices
- Look for object retention paths and potential leaks.
- Combine runtime monitoring with GC statistics for actionable insights.
61 What is exception handling in C# (try/catch/finally) and when to use custom exceptions?
What is exception handling in C# (try/catch/finally) and when to use custom exceptions?
Exception Handling in C#
C# uses try/catch/finally blocks to handle runtime errors gracefully. try encloses code that might throw exceptions, catch handles the exception, and finally runs cleanup code regardless of success or failure.
Custom Exceptions
Create custom exceptions when you need domain-specific error types for clarity and structured handling. Inherit from Exception or ApplicationException. Avoid excessive custom exceptions to prevent maintenance overhead.
Example:
try {
ProcessOrder(order);
} catch (OrderNotFoundException ex) {
Console.WriteLine($"Order not found: {ex.Message}");
} finally {
CleanUpResources();
}
public class OrderNotFoundException : Exception {
public OrderNotFoundException(string msg) : base(msg) {}
} 62 What are exception filters and how do they help?
What are exception filters and how do they help?
Exception Filters
Exception filters (catch <Exception> when(condition)) allow conditional catching of exceptions without unwinding the stack immediately. They are useful for diagnostic logging or handling specific scenarios.
Example:
try {
ProcessData();
} catch (Exception ex) when (ex is IOException) {
Console.WriteLine("IO Exception occurred");
}Filters improve clarity and prevent over-catching unrelated exceptions.
63 Best practices for logging, error propagation, and not swallowing exceptions.
Best practices for logging, error propagation, and not swallowing exceptions.
Best Practices for Logging and Error Propagation
- Log sufficient context, including stack traces, correlation IDs, and relevant variable state.
- Avoid swallowing exceptions silently; always rethrow with
throw;to preserve stack trace. - Centralize logging with structured loggers, and avoid leaking sensitive data.
Example:
try {
DoWork();
} catch (Exception ex) {
logger.LogError(ex, "Error processing request");
throw; // preserve stack trace
} 64 Debugging techniques: breakpoints, watch windows, Debug and Trace classes.
Debugging techniques: breakpoints, watch windows, Debug and Trace classes.
Debugging Techniques
Use IDE breakpoints, conditional breakpoints, watch windows, and locals to inspect runtime state. Debug and Trace classes allow instrumentation of code for development or production monitoring.
Example:
Debug.WriteLine("Current value: " + value);
Trace.TraceInformation("Info logged");Combine breakpoints and watch windows for complex issues; use logging and profiling for production environments.
65 What is unit testing and common test frameworks for C# (xUnit, NUnit, MSTest)?
What is unit testing and common test frameworks for C# (xUnit, NUnit, MSTest)?
Unit Testing in C#
Unit tests validate small, isolated code units to ensure correctness. Common frameworks are xUnitNUnit, and MSTest. Choose based on project ecosystem, CI integration, and features.
Example:
[Fact] // xUnit attribute
public void Add_TwoNumbers_ReturnsSum() {
var result = Calculator.Add(2, 3);
Assert.Equal(5, result);
} 66 How to mock dependencies in tests (Moq, interfaces)?
How to mock dependencies in tests (Moq, interfaces)?
Mocking Dependencies
Use interfaces and dependency injection to decouple code. Libraries like Moq allow mocking behaviors for testing, enabling verification of interactions without relying on concrete implementations.
Example:
var mockService = new Mock();
mockService.Setup(s => s.Send(It.IsAny<string>())).Returns(true);
var controller = new EmailController(mockService.Object);
controller.SendEmail("test@test.com");
mockService.Verify(s => s.Send("test@test.com"), Times.Once);
67 Explain Test-Driven Development (TDD) briefly and practical workflow.
Explain Test-Driven Development (TDD) briefly and practical workflow.
Test-Driven Development (TDD)
TDD is a development methodology following Red-Green-Refactor: first write a failing test (Red), implement the minimal code to pass (Green), then refactor for clarity or efficiency.
Practical Workflow:
- Write a unit test for a small feature.
- Run the test to ensure it fails.
- Implement the minimal code to pass the test.
- Refactor code while keeping tests passing.
- Repeat for the next feature or behavior.
TDD encourages small, testable designs and continuous automated verification.
68 Why SOLID principles matter and short examples for each in C#.
Why SOLID principles matter and short examples for each in C#.
SOLID Principles in C#
- S – Single Responsibility Principle (SRP): A class should have only one reason to change.
public class InvoicePrinter {
public void Print(Invoice invoice) { /* printing logic */ }
}public abstract class Shape {
public abstract double Area();
}
public class Circle : Shape { /* override Area */ }Stream fs = new FileStream("file.txt"); // works with any Streampublic interface IReadable { void Read(); }
public interface IWritable { void Write(); }public class OrderService {
private readonly IRepository repo;
public OrderService(IRepository repo) { this.repo = repo; }
} 69 Common design patterns used in C# (.NET) — Repository, Dependency Injection, Singleton, Factory, Observer.
Common design patterns used in C# (.NET) — Repository, Dependency Injection, Singleton, Factory, Observer.
Common Design Patterns in C#
- Repository: Abstracts data access.
public interface ICustomerRepo { Customer GetById(int id); }services.AddScoped<ICustomerRepo, CustomerRepo>();public sealed class Logger {
private static readonly Logger instance = new Logger();
public static Logger Instance => instance;
}public class ShapeFactory { public Shape Create(string type) { /* ... */ } }event Action OnChanged; 70 How to structure code for maintainability (separation of concerns, layering, DI)?
How to structure code for maintainability (separation of concerns, layering, DI)?
Structuring Code for Maintainability
- Use layered architecture: UI → Business Logic → Data Access.
- Apply Dependency Injection for decoupling.
- Follow SOLID principles for extensibility.
- Keep classes small, cohesive, and focused.
Example:
public class CustomerController {
private readonly ICustomerService service;
public CustomerController(ICustomerService service) { this.service = service; }
} 71 Performance optimizations: avoiding allocations, pooling, value types vs reference types.
Performance optimizations: avoiding allocations, pooling, value types vs reference types.
Performance Optimizations in C#
- Avoid unnecessary allocations (e.g., use
ArrayPool<T>). - Reuse objects when possible (object pooling).
- Use value types for small, frequently used data structures to avoid heap allocation.
- Profile hotspots before optimizing prematurely.
- Leverage
Span<T>andref structfor high-performance memory access.
Example:
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
try {
// use buffer
} finally {
pool.Return(buffer);
} 72 How to call unmanaged/native code from C# (P/Invoke) and common pitfalls?
How to call unmanaged/native code from C# (P/Invoke) and common pitfalls?
Calling Native Code (P/Invoke)
Use [DllImport] to call unmanaged libraries. Be careful with parameter marshaling and memory ownership.
Example:
[DllImport("user32.dll")]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
MessageBox(IntPtr.Zero, "Hello", "Title", 0);- Ensure correct calling conventions.
- Handle platform-specific differences (x86 vs x64).
- Avoid memory leaks by freeing unmanaged resources properly.
73 How to work with COM objects from C#?
How to work with COM objects from C#?
Working with COM Objects
- Use COM Interop via runtime callable wrappers (RCW) or
tlbimpto generate interop assemblies. - Register COM servers if required.
- Release COM references explicitly with
Marshal.ReleaseComObject().
Example:
var excel = new Microsoft.Office.Interop.Excel.Application();
// ... use Excel COM object
Marshal.ReleaseComObject(excel); 74 Differences between .NET Framework, .NET Core and modern .NET and cross-platform considerations.
Differences between .NET Framework, .NET Core and modern .NET and cross-platform considerations.
.NET Framework vs .NET Core vs Modern .NET (5+)
These three represent the evolution of Microsoft’s .NET ecosystem. Understanding their differences is crucial for choosing the right runtime and for cross-platform considerations.
| Feature | .NET Framework | .NET Core | .NET (5+) |
|---|---|---|---|
| Platform | Windows only | Windows, Linux, macOS | Unified cross-platform runtime |
| Status | Legacy (no new features, only security patches) | Superseded by .NET 5+ | Current and future direction |
| Performance | Lower, older JIT optimizations | Much faster, modular runtime | High efficiency, continuous optimization |
| Deployment | Installed via Windows system | Side-by-side, self-contained deployments | Flexible deployment (containers, cloud-native, self-contained) |
| Use Cases | Existing enterprise & legacy apps | Cross-platform and cloud apps (2016–2020) | All new development — web, desktop, mobile (via MAUI) |
Cross-Platform Considerations:
- .NET Framework: Limited to Windows. Not suitable for Linux/macOS or container-based apps.
- .NET Core: First to introduce true cross-platform support, but now replaced by modern .NET.
- Modern .NET (5+): Unified platform covering web (ASP.NET Core), APIs, desktop (WPF/WinForms on Windows, MAUI cross-platform), cloud-native, IoT, and even mobile via MAUI/Xamarin.
Best Practice: For new applications, always choose modern .NET (5/6/7+). Use .NET Framework only when maintaining Windows-specific legacy apps. .NET Core is effectively in maintenance mode — use .NET 6+ for long-term support.
75 How to containerize/ship C# applications (Docker) and cloud considerations?
How to containerize/ship C# applications (Docker) and cloud considerations?
Containerizing and Shipping C# Apps with Docker
- Use multi-stage builds: first compile in SDK image, then ship with runtime-only image.
- Follow 12-factor app principles (config via env vars, stateless).
- Set up health checks, logging, and secrets management for cloud deployment.
- Optimize images by trimming unused layers.
Example Dockerfile:
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "MyApp.dll"]Unlock All Answers
Subscribe to get unlimited access to all 75 answers in this module.
Subscribe NowNo questions found
Try adjusting your search terms.