Interview Preparation

C# Questions

Interview-ready C# questions covering .NET, OOP, LINQ, and real-world coding.

Topic progress: 0%
1

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

FeatureWhat it gives you
Static typingCompile-time type checking and better tooling (IntelliSense, refactoring)
Object-orientedClasses, interfaces, inheritance, polymorphism, encapsulation
Garbage collectionAutomatic memory management via the CLR
GenericsType-safe reusable code (List<T>, Dictionary<TKey,TValue>)
LINQDeclarative querying over collections and data sources
async/awaitCleaner asynchronous programming for IO-bound code
Rich standard libraryWide APIs for IO, networking, threading, cryptography, etc.
Cross-platform .NETRun on Windows, Linux, macOS via .NET 5/6/7+
InteroperabilityP/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.

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:

SignatureMeaning
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)?

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# aliasCLR typeDescription
boolSystem.BooleanBoolean true/false
byteSystem.ByteUnsigned 8-bit integer
sbyteSystem.SByteSigned 8-bit integer
shortSystem.Int16Signed 16-bit integer
ushortSystem.UInt16Unsigned 16-bit integer
intSystem.Int32Signed 32-bit integer
uintSystem.UInt32Unsigned 32-bit integer
longSystem.Int64Signed 64-bit integer
ulongSystem.UInt64Unsigned 64-bit integer
floatSystem.SingleSingle-precision floating point
doubleSystem.DoubleDouble-precision floating point
decimalSystem.DecimalHigh-precision decimal for financial calculations
charSystem.CharUTF-16 character
stringSystem.StringImmutable sequence of characters
objectSystem.ObjectBase 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);       // True

Nullable & 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?

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.

AspectValue typeReference type
Examplesint, double, bool, DateTime, structclass, string, object, arrays, delegates
StorageStored directly (stack or inline in other objects)Stored on the heap; variable holds a reference
AssignmentCopies the value (deep copy for fields)Copies the reference (both refer to same object)
Default nullabilityCannot be null (unless Nullable<T>)Can be null (reference may point to nothing)
Garbage collectionNo; storage reclaimed when scope endsManaged by GC
Typical useSmall, immutable, short-lived dataComplex 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 100

Boxing 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?

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.

NotationMeaning
stringNon-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 disable

Important 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

AspectValue Nullable (T?)Nullable Reference (T?, C# 8+)
Runtime representationNullable<T> wraps value and HasValue flagNo runtime wrapper; just normal reference which may be null
Compile-time helpType system enforces non-nullability for value types by designStatic analysis warns about possible nulls when enabled
How to checkHasValue / compare to nullnull-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>enable with gradual fixes to suppress warnings incrementally.
  • Use ???. (null-conditional), and ! (null-forgiving) thoughtfully.
6

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.Logger vs CompanyB.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 using to 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

  1. Name namespaces to reflect company/project and module (e.g., Contoso.Payment.Processing).
  2. Keep namespaces focused and avoid deeply nested types unless necessary.
  3. Organize folder structure to mirror namespaces for discoverability.
  4. 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 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 var when 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>() vs var 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 does Get return?).
  • When explicitness improves readability for the team or public APIs.

Rules and behavior

  • The type must be determinable at compile-time; var cannot be used for fields (only local variables), method parameters, or return types.
  • Using var does 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.

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 int

Why 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

  1. 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.
  2. Boxing in APIs: Be careful with APIs that take object or params object[] — passing value types will cause boxing.
  3. 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>, and ref struct to work with memory efficiently without heap allocations.
9

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 → float

Explicit 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 cast

Reference 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 incompatible

Pattern 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 as or is to check types safely.
10

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 overridden

GetHashCode

When overriding Equals, always override GetHashCode to preserve hash-based collection invariants (e.g., DictionaryHashSet).

Guidelines

  • Use = to assign values, == or Equals() to compare. Understand whether == is overloaded for the types you compare.
  • Prefer Equals or Object.Equals when 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>, override Equals(object) and GetHashCode, and consider overloading == and != for consistency.
11

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

ModifierWhere visibleTypical use
publicAny code that can reference the assembly/typePublic API surface, libraries, DTOs
privateOnly within the declaring typeImplementation details, helper fields/methods
protectedDeclaring type and derived types (inheritance)Allow subclasses to access behavior/state
internalAny code in the same assemblyTypes/members for internal use within a project
protected internalEither derived types OR same assemblyBroad access for derived types and intra-assembly consumers
private protectedDerived types in the same assemblyRestrict 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 internal if 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

  1. Prefer the least permissive accessibility that still allows the required usage — start private and widen only when necessary.
  2. Use internal for implementation types you don’t want to expose from a library (combine with InternalsVisibleTo for test assemblies if needed).
  3. Avoid exposing fields as public; prefer properties to preserve encapsulation and allow future validation.
  4. Use protected when 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?

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

AspectStaticInstance
State lifetimeApplication/domain lifetime (shared)Per-object lifetime
TestabilityHarder to mock/test without wrappersEasier to inject via DI
Thread-safetyRequires explicit synchronization for mutable stateCan avoid shared-state issues

Pitfalls & best practices

  1. Avoid hidden global state: mutable static fields act like globals — they make reasoning and testing harder. Prefer immutable static data when possible.
  2. 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.
  3. Make thread-safety explicit: If static members hold mutable state, protect access with locks or use concurrent collections.
  4. 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).

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 override

Abstraction

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?

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).

AspectClass (reference type)Struct (value type)
StorageHeap (managed by GC)Stack or inline within other objects
AssignmentCopies reference (shared object)Copies entire value (independent)
InheritanceSupports single inheritanceCannot inherit from another struct (but can implement interfaces)
Default constructorCan define parameterless constructor (behavior depends on C# version)Always has an implicit parameterless constructor that initializes fields to default
Use-caseLarge, mutable, identity-bearing objectsSmall, 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

  1. Keep structs small and immutable — large structs incur copy overhead.
  2. Prefer classes for most domain models and service objects.
  3. 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?

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 base keyword accesses members or constructors of the immediate base class.
  • You can use virtual on base methods and override on 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

  1. 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.
  2. Composition: Encapsulate behavior in other objects and delegate calls. "Has-a" relationships reduce coupling compared to "is-a" inheritance.
  3. 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.

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

AspectOverloadingOverriding
When resolvedCompile-timeRuntime (virtual dispatch)
RequirementDifferent parameter signatures in same typeDerived method must match signature and use override for a virtual/abstract base
GoalProvide multiple ways to call similar operationProvide specialized behavior for subtypes
Polymorphism typeStatic (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 omit new). 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

  1. Use overloading to provide convenient variations of a method (different parameter sets), but keep overloads semantically consistent to avoid confusion.
  2. 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?

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

FeatureAbstract ClassInterface
Can contain fieldsYes (state)No (no instance fields)
ConstructorsYesNo
Multiple inheritanceNo (single base class)Yes (implement many interfaces)
Default implementationYesPossible since C# 8 (default methods)
Use forShared base behavior/stateCross-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

  1. Prefer interfaces for public-facing contracts (APIs) and for types you will mock or substitute in tests.
  2. Use abstract classes when you have a clear, tight family of types that share implementation and internal state.
  3. Favor composition over inheritance when sharing behavior across unrelated types to reduce coupling.
18

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

  1. Call a base constructor — initialize base class state from a derived class constructor.
  2. Call a base method — when overriding, you may still want to use the base behavior as part of the override.
  3. Access hidden members — if a derived member hides a base member (new), base can 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

  • base only 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#?

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 get

Auto-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 property

Access 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 init for immutable objects that should only be set during object initialization.
  • Remember to implement GetHashCode and Equals appropriately when properties participate in equality semantics.
20

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 indexer

Indexers 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-only

Use 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 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

AspectDelegateEvent
Direct invocation by outsidersAllowed (if delegate field is public)Not allowed — only the declaring class can raise
Subscription modelYes (multicast)Yes (multicast with encapsulation)
Best practiceUse privately or for internal plumbingExpose 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 event rather than raw delegate fields to protect invocation control.
  • Use the standard event pattern event EventHandler<TEventArgs> or EventHandler and create an OnXxx protected virtual method to raise the event.
  • For modern APIs, consider Action<...> or Func<...> delegates for private callbacks but keep public APIs event-based.
22

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?

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.

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?

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?

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

  • is expression 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.

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?

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?

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 using for short-lived resources to ensure deterministic cleanup.
  • Implement IDisposable for 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?

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 AttributeUsage to 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?

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

TypeWhen to useCharacteristics
Array (T[])Fixed-size collections with fastest indexed accessContiguous memory, O(1) index, fixed length
List<T>Dynamic array for most general-purpose needsAmortized O(1) append, O(1) index, O(n) insert/remove middle
LinkedList<T>Frequent inserts/removes in the middle and stable node referencesO(1) insert/remove given node, O(n) indexing, poor locality
Dictionary<TKey,TValue>Key → value lookupHash-based, O(1) average lookup/insert, requires stable key hash
HashSet<T>Maintain unique items and fast membership checksHash-based, O(1) average contains/add
Queue<T>FIFO processingEnqueue/Dequeue O(1)
Stack<T>LIFO processing or recursion emulationPush/Pop O(1)
SortedList / SortedDictionaryOrdered maps by keyTree-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 custom IEqualityComparer if 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?

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.

OperationArrayList<T>LinkedList<T>
Indexing (get/set)O(1)O(1)O(n)
AppendO(1) (if pre-sized) / O(n) if resizing manuallyAmortized O(1)O(1) if you have tail ref
Insert/remove middleO(n) (shift elements)O(n) (shift elements)O(1) given node reference
MemoryMinimal overheadExtra overhead for capacity vs countHigh 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?

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

AspectHashtableDictionary<TKey,TValue>
GenericNo (stores object)Yes — type parameters for key and value
Type safetyNo — requires boxing/unboxing for value types and castsYes — compile-time safety, no casts
PerformanceLess efficient due to boxing/casts for value typesBetter performance and fewer allocations
API modernityOlder APIModern, 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 Hashtable when working with very old code or APIs that require it (rare).

Best practices for Dictionary

  1. Use TryGetValue to avoid throwing on missing keys.
  2. Provide an initial capacity when you can to avoid rehashing overhead (new Dictionary<TKey,TValue>(capacity)).
  3. Consider a custom IEqualityComparer<TKey> for specialized hashing or case-insensitive keys.
34

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

AspectIEnumerable<T>IQueryable<T>
Execution modelDeferred execution, runs in-memory (LINQ-to-Objects)Deferred execution, query provider translates Expression tree to remote execution
Use caseIn-memory collections, in-process filteringDatabase/remote providers that translate queries to SQL or other languages
Where clausesExecuted in CLR after enumerationTranslated 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

  1. Prefer forming as much of the query as possible against IQueryable so expensive filters happen in the database.
  2. Be careful when mixing IQueryable and IEnumerable — calling a method that forces enumeration (e.g., AsEnumerable()ToList()) may prematurely bring data into memory.
  3. When building provider-agnostic helpers, accept IEnumerable<T> unless you explicitly need provider translation.
35

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: IQueryable providers 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?

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 IQueryable side so filters and aggregates execute in the database.
37

How do you handle large data sets efficiently (streaming, pagination)?

Core strategies

  1. Streaming / lazy enumeration: Use IEnumerable<T> with yield return or data readers to process items one-by-one without loading all data into memory.
  2. Pagination: Query and fetch only a page of items at a time (skip/take at DB level), suitable for UIs and REST APIs.
  3. Batch processing: Chunk data into manageable batches for processing, committing, or sending over the network.
  4. 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.

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 IEnumerable or IEnumerator.

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.

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.Run for offloading CPU work to background threads if needed.
  • Exception handling: exceptions thrown in async methods propagate via the returned Task and can be observed with await or task.ContinueWith.
  • Context capture: await by default captures the current synchronization context and resumes on it. Use ConfigureAwait(false) in library code to avoid capturing UI or request contexts and improve performance.

Best practices

  1. Use async all the way: avoid mixing synchronous blocking calls (e.g., .Result.Wait()) with async to prevent deadlocks.
  2. Prefer Task<T> over void for async methods; async void is only for event handlers.
  3. Consider ValueTask when minimizing allocations for hot-path async methods that often complete synchronously.
40

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.

ConceptThreadTaskValueTask
NatureOS threadLogical async operationValue-type wrapper for async result
Creation costHighRelatively low (uses thread pool or I/O completion)Lower allocations when sync completion occurs
Use with asyncManual threadingPrimary building block for async/awaitOptimization tool for hot paths

When to use which

  • Prefer Task and async/await for most asynchronous/I/O scenarios.
  • Use Thread directly only when you need specialized thread behavior, thread-local stacks, or long-lived dedicated threads — otherwise use thread pool via Task.Run.
  • Consider ValueTask for 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?

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 Task for 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/ForEach for 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

APIGood forNotes
TaskAsynchronous work, IO-bound or CPU-bound tasks with orchestrationCombinators (WhenAll/WhenAny), cancellation, continuations
ParallelCPU-bound loops (For, ForEach)Automatic partitioning, uses thread pool threads
PLINQParallel LINQ queries over in-memory collectionsQuery-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 MaxDegreeOfParallelism when 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)?

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() — throw OperationCanceledException.
  • 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 OperationCanceledException to perform cleanup and avoid treating cancellations as faults in logs.
43

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 .Result or .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.

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

NeedRecommended primitive
Simple in-process mutual exclusionlock (Monitor)
Cross-process synchronizationMutex
Async-friendly throttling / limited concurrencySemaphoreSlim (with WaitAsync)
Many readers, few writersReaderWriterLockSlim
Simple atomic countersInterlocked

Practical tips

  • Keep critical sections small to reduce contention.
  • Avoid locking on publicly accessible objects (lock(this)) — prefer private readonly objects.
  • Prefer SemaphoreSlim for async scenarios instead of blocking primitives.
45

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 .Result or .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

  1. Consistent lock ordering: Always acquire multiple locks in the same global order across your codebase.
  2. Minimize lock scope: Keep critical sections as short as possible.
  3. Prefer non-blocking patterns: Use concurrent collections (ConcurrentDictionary, ConcurrentQueue) or lock-free algorithms where possible.
  4. Timeouts and try-lock patterns: Use Monitor.TryEnter with timeouts to recover or log when contention is extreme.
  5. Avoid synchronous blocking of async code: Use async/await and ConfigureAwait(false) appropriately instead of .Result or .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?

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.IncrementCompareExchange for 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

  1. Favor immutable data and pure functions where possible — they simplify reasoning about concurrency.
  2. Encapsulate synchronization within a small module so the rest of the codebase uses a safe API.
  3. Prefer higher-level concurrency constructs (TPL Dataflow, Channels) rather than ad-hoc locking.
  4. 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)?

Key APIs

System.IO provides several levels of abstraction for file IO:

  • File and FileInfo — 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 FileStream with useAsync: true for efficient async IO on Windows.
48

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).

Format differences

AspectXMLJSON
VerbosityMore verbose, tag-basedMore compact, key/value pairs
SchemaSupports XSD, strong schema validationNo built-in schema standard (JSON Schema exists but less commonly enforced)
AttributesSupports attributes and element textNo attributes; everything is a value or nested object
Use casesDocument formats, SOAP, config with schemaAPIs, 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?

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 BufferedStream or set a buffer size on FileStream to 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/WriteAsync for non-blocking IO; for high-performance scenarios use FileStream with useAsync: 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

  1. Avoid allocating per-iteration buffers — reuse or rent buffers.
  2. Batch small writes into larger buffers where possible.
  3. Profile and measure — IO bottlenecks are often disk/network bound, not CPU bound.
51

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?

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.

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?

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.

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?

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?

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?

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?

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?

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?

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?

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 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

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)?

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)?

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.

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:

  1. Write a unit test for a small feature.
  2. Run the test to ensure it fails.
  3. Implement the minimal code to pass the test.
  4. Refactor code while keeping tests passing.
  5. 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#.

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 */ }
    }
  • O – Open/Closed Principle (OCP): Classes should be open for extension but closed for modification.
  • public abstract class Shape {
        public abstract double Area();
    }
    public class Circle : Shape { /* override Area */ }
  • L – Liskov Substitution Principle (LSP): Subtypes should replace supertypes without breaking behavior.
  • Stream fs = new FileStream("file.txt"); // works with any Stream
  • I – Interface Segregation Principle (ISP): Prefer small, specific interfaces over large, general ones.
  • public interface IReadable { void Read(); }
    public interface IWritable { void Write(); }
  • D – Dependency Inversion Principle (DIP): Depend on abstractions, not concretions.
  • 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 in C#

  • Repository: Abstracts data access.
  • public interface ICustomerRepo { Customer GetById(int id); }
  • Dependency Injection (DI): Inject dependencies instead of hardcoding them.
  • services.AddScoped<ICustomerRepo, CustomerRepo>();
  • Singleton: Ensures a single instance.
  • public sealed class Logger {
        private static readonly Logger instance = new Logger();
        public static Logger Instance => instance;
    }
  • Factory: Creates objects without exposing instantiation logic.
  • public class ShapeFactory { public Shape Create(string type) { /* ... */ } }
  • Observer: Publish/subscribe for event-driven systems.
  • event Action OnChanged;
70

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 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> and ref struct for 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?

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#?

Working with COM Objects

  • Use COM Interop via runtime callable wrappers (RCW) or tlbimp to 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.

.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+)
PlatformWindows onlyWindows, Linux, macOSUnified cross-platform runtime
StatusLegacy (no new features, only security patches)Superseded by .NET 5+Current and future direction
PerformanceLower, older JIT optimizationsMuch faster, modular runtimeHigh efficiency, continuous optimization
DeploymentInstalled via Windows systemSide-by-side, self-contained deploymentsFlexible deployment (containers, cloud-native, self-contained)
Use CasesExisting enterprise & legacy appsCross-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?

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"]