.NET Framework / Legacy App Developer — Interview Questions
In short: Both compile to the same IL and run on the same CLR with the same libraries, so performance is the same.
- Differences are syntax and features: VB.NET uses English-like keywords, is case-insensitive, line-based with
Endblocks, has XML literals,Mynamespace,WithEvents/Handlesand optional late binding; - C# uses C-style syntax, is case-sensitive and gets new language features every year while VB.NET has been feature-stable since 2020.
- Most new projects use C#; VB.NET is mainly maintained in legacy systems.
In short: Option Explicit On requires variables to be declared.
- Option Strict On disallows implicit narrowing conversions and late binding — catching type errors at compile time (strongly recommended).
- Option Infer On lets
Dim x = 5infer the type from the initializer. - Set them per project (Compile settings) or per file at the top.
In short: A Sub performs an action and returns no value (C# void method).
- A
Functionreturns a value withReturn(or by assigning to the function name in old code). - Both can take ByVal/ByRef, Optional and ParamArray parameters.
In short: ByVal (the default) passes a copy of the value — for reference types, a copy of the reference, so the method can modify the object but not replace the caller's variable.
- ByRef passes the variable itself, so assigning a new value inside the method changes the caller's variable (C#
ref).
In short: And/Or always evaluate both operands (and also act as bitwise operators on integers).
AndAlso/OrElseshort-circuit: the second operand is evaluated only if needed.- Use
AndAlsofor conditions likeIf obj IsNot Nothing AndAlso obj.IsActive Thento avoid NullReferenceException.
In short: Declare with Event, raise with RaiseEvent.
- Subscribe statically by declaring the field
WithEventsand adding aHandles obj.EventNameclause to the handler (common in WinForms/Web Forms code-behind), or dynamically withAddHandler obj.Event, AddressOf HandlerandRemoveHandler(like C#+=/-=).
In short: DirectCast performs a type cast requiring an inheritance/interface relationship and throws InvalidCastException on failure (fast, like C# (T)x).
CTypeperforms any defined conversion including VB conversions (string to integer) and user-defined conversions.TryCastreturnsNothinginstead of throwing (reference types only, like C#as).
In short: Stabilise first: source control, Option Strict On, logging, tests on critical flows.
- Extract business logic from code-behind into class libraries (VB or new C#), target
netstandard2.0where possible so both old and new apps can use them. - Put a reverse proxy (YARP/IIS ARR) in front and rebuild pages/features in ASP.NET Core (C#) one area at a time, sharing authentication and the database (strangler fig).
- Retire Web Forms pages as their replacements go live.
A value type variable holds the data itself (int, bool, decimal, DateTime, any struct/enum). Assigning it copies the value. A reference type variable holds a reference to an object on the managed heap (class, string, arrays, delegates, record). Assigning it copies the reference, so two variables can point to the same object.
var a = new List<int>(); var b = a; b.Add(1); // a.Count == 1 — same object
int x = 1; int y = x; y++; // x is still 1Value types are stored inline (stack for locals, inside the parent object for fields); reference-type objects always live on the heap and are cleaned up by the GC.
string and StringBuilder?In short: string is immutable: every concatenation creates a new string object, so building a string in a loop causes many allocations (O(n²) copying).
StringBuilderkeeps a mutable buffer and appends in place; callToString()once at the end.- Use
StringBuilderfor loops or many appends, plain strings/interpolation for a handful of pieces.
const vs readonly vs static readonly.const: compile-time constant, implicitly static, only primitives/strings; the value is baked into callers at compile time.readonly: instance field assigned at declaration or in a constructor; can differ per object.static readonly: one value per type, computed at runtime (e.g.static readonly HttpClient Client = new()).
Gotcha: if library A has public const int Max = 10 and app B uses it, changing it to 20 in A does nothing in B until B is recompiled. Use static readonly for values that may change.
In short: Boxing converts a value type to object (or an interface) by allocating a heap object and copying the value; unboxing casts it back to the exact value type.
- It costs an allocation, a copy and GC pressure.
- Typical sources: non-generic collections (
ArrayList),string.Formatwith value-type arguments on old APIs, calling interface methods on structs via the interface. - Generics remove most boxing.
== and .Equals()?In short: For reference types, == compares references unless the operator is overloaded; Equals() is virtual and can be overridden to compare content.
stringoverloads both to compare text.- Records generate value-based
Equalsand==. - For value types,
Equalscompares content. - When you override
Equals, also overrideGetHashCode(objects that are equal must have the same hash code, or dictionaries break).
ref, out and in parameters?All three pass an argument by reference:
ref— must be initialised before the call; the method can read and change it.out— need not be initialised; the method must assign it (used byTryParse).in— read-only reference; avoids copying large structs.
decimal instead of double for money?In short: double is binary floating point and can't represent many base-10 fractions exactly (0.1 + 0.2 != 0.3), so totals drift by tiny amounts that show up in invoices and reports.
decimalstores base-10 digits exactly with 28–29 significant digits.- Trade-off:
decimalis slower and uses 16 bytes, so usedoublefor scientific/graphics maths anddecimalfor money and quantities that must add up exactly. - In SQL Server map it to
DECIMAL(18,2), notFLOAT.
Parse, TryParse and Convert?In short: int.Parse("x") throws FormatException for bad input (and ArgumentNullException for null).
int.TryParse(s, out var n)returnsfalseinstead of throwing — use it for user input.Convert.ToInt32(value)accepts many types (object, double, string), returns 0 fornull, and rounds doubles to the nearest even number (Convert.ToInt32(2.5)→ 2) whereas a cast truncates ((int)2.9→ 2).- Pass a
CultureInfowhen parsing numbers and dates from files or APIs.
Dictionary and Hashtable?In short: Dictionary<TKey,TValue> is generic: type-safe, no boxing, faster; a missing key with the indexer throws KeyNotFoundException (use TryGetValue).
Hashtableis the old non-generic version storingobjectkeys/values: needs casts, boxes value types, and returnsnullfor a missing key.- Both are hash tables with O(1) average lookup.
- For multiple threads use
ConcurrentDictionary.
In short: A multidimensional (rectangular) array int[,] is one object where every row has the same length, accessed as a[r, c].
- A jagged array
int[][]is an array of arrays — each row is a separate array and can have a different length, accessed asa[r][c]. - Jagged arrays are more flexible and often faster in .NET; rectangular arrays suit fixed grids such as matrices.
IEnumerable, ICollection, IList and IQueryable.IEnumerable<T>: forward-only iteration (foreach), deferred execution with LINQ to Objects, runs in memory.ICollection<T>: addsCount,Add,Remove,Contains.IList<T>: adds index access andInsert.IQueryable<T>: builds an expression tree that a provider (EF Core) translates to SQL and runs in the database.
db.Orders.Where(o => o.Total > 100).ToList(); // IQueryable: WHERE in SQL
db.Orders.AsEnumerable().Where(o => o.Total > 100).ToList(); // loads ALL rows then filters in memoryIn short: A delegate is a type-safe function pointer (Func, Action, custom delegate).
- It can be multicast.
- An event wraps a delegate so outside code can only
+=/-=handlers; only the declaring class can raise it. - This prevents subscribers from wiping out other handlers (
= null) or raising the event themselves. - Pattern: `public event EventHandler
? - Changed;
and raise withChanged?.Invoke(this, args)`.
In short: No new thread is created by await.
- The compiler turns the method into a state machine.
- When it hits
awaiton an incomplete task, the method returns to its caller and the thread goes back to the pool. - When the I/O completes, a continuation resumes the state machine — on the captured synchronisation context if there is one (UI thread), otherwise on a thread-pool thread.
- CPU-bound work still needs
Task.Runor parallelism to move off the current thread.
Task.WhenAll and awaiting tasks one by one?In short: Awaiting sequentially makes total time the sum of the calls; starting all tasks and then await Task.WhenAll(...) makes it roughly the longest single call.
WhenAllthrows the first exception when awaited, but the returned task'sExceptionholds all of them.- Use
WhenAnyfor "first to finish" or timeouts.
async void dangerous?In short: Callers can't await it, so they can't know when it finishes, and an exception thrown inside crashes the process (it is raised on the synchronisation context instead of being stored in a Task).
- Use
async Taskeverywhere except event handlers.
IDisposable and the using statement? Dispose vs Finalize?In short: IDisposable.Dispose() releases unmanaged resources (connections, file handles) deterministically.
usingguaranteesDisposeis called even if an exception is thrown (compiles totry/finally).- A finaliser is called by the GC at an unpredictable time and costs an extra GC cycle; it is only a safety net for raw unmanaged handles.
- In
DisposecallGC.SuppressFinalize(this)if a finaliser exists. - Most code just wraps
SafeHandleand never writes a finaliser.
In short: The heap is split into Gen 0 (new objects, collected very often and cheaply), Gen 1 (survivors of Gen 0, a buffer), and Gen 2 (long-lived objects, collected rarely and expensively).
- Most objects die young, so collecting Gen 0 frequently is efficient.
- Objects of 85,000 bytes or more go to the Large Object Heap, collected with Gen 2. .NET has workstation and server GC modes, plus background (concurrent) GC for Gen 2.
In short: Derive from Exception, end the name with Exception, provide the standard constructors (parameterless, message, message + inner exception) plus constructors/properties for useful data (e.g. Balance, Requested).
- Create one only when callers need to catch that specific situation (e.g.
InsufficientFundsException→ show a friendly message or return 409); - otherwise throw the built-in ones (
ArgumentException,InvalidOperationException). - Always pass the inner exception when wrapping.
lock(sugar forMonitor.Enter/Exit): mutual exclusion inside one process, synchronous only.SemaphoreSlim: limits concurrency to N and supportsWaitAsync, so use it when you mustawaitinside the critical section.Interlocked: atomic operations on single variables (increment, compare-exchange), lock-free.ConcurrentDictionary/ConcurrentQueue/Channel<T>: built-in thread-safe structures.- Immutability: data that never changes needs no locks.
- Across processes/servers: database locks, distributed locks (Redis, blob leases),
Mutexfor the same machine.
In short: Classic case: in a UI app or old ASP.NET (with a synchronisation context), code calls task.Result.
- The blocked thread holds the context; the awaited method's continuation needs the same context to resume, so both wait forever.
- Fixes: go async all the way (
awaitinstead of.Result), useConfigureAwait(false)in library code, and never mix blocking and async calls. - ASP.NET Core has no synchronisation context, but blocking still causes thread-pool starvation under load.
+= on a string and takes minutes. What do you do?In short: String concatenation in a loop copies the whole string every time, so the work is O(n²).
- Replace it with a
StringBuilder, or better, stream the rows straight to the output (StreamWriter, orPipeWriterin ASP.NET Core) so the full file is never held in memory. - Measure before and after with a stopwatch or BenchmarkDotNet.
- For huge exports, page the database query and write as you read.
- Confirm the trend with
dotnet-counters(GC heap size, Gen 2 count, LOH size). - Take memory dumps a few minutes apart with
dotnet-dump collect(ordotnet-gcdump) and compare which types grow, using Visual Studio, PerfView ordotnet-dump analyze(dumpheap -stat,gcroot). - Usual culprits: static collections or caches with no eviction, event handlers never unsubscribed,
HttpClientcreated per request, undisposed streams/DbContexts, large byte arrays on the LOH, unboundedMemoryCache. - Fix (size-limited cache,
IHttpClientFactory,using, weak events), then load-test to confirm.
In short: count++ is read, add, write — three steps.
- Two threads can read the same value and both write value+1, so one increment is lost (a race condition).
- Fix with
Interlocked.Increment(ref count), alock, or a concurrent type. - If the counter must survive restarts or work across several servers, keep it in the database (`UPDATE ...
- SET Count = Count + 1
) or in RedisINCR` instead.
In short: The Common Language Runtime is .NET's execution engine.
- It loads assemblies, verifies type safety, JIT-compiles IL to native code, manages memory with the garbage collector, handles exceptions, threads and synchronisation, provides interop with native code and exposes debugging/profiling services.
- Code running under it is "managed code".
In short: CTS (Common Type System) defines how types are declared and used in .NET — value vs reference types, inheritance, members, visibility — so all languages share one type system (C# int = VB.NET Integer = System.Int32).
- CLS (Common Language Specification) is a subset of CTS rules that every .NET language supports, so a CLS-compliant library can be used from any language (e.g. avoid public unsigned types or names differing only in case).
In short: IL is the CPU-independent, stack-based intermediate code the C#/VB.NET compilers produce, stored with metadata in assemblies.
- At runtime the JIT compiles each method's IL to native machine code on its first call, optimised for the actual CPU;
- tiered compilation starts with quick code and re-compiles hot methods with full optimisation.
- ReadyToRun and Native AOT precompile to reduce startup time.
In short: .NET Framework (2002–2019) is Windows-only, installed machine-wide, and now only maintained (4.8.1 last);
- it has Web Forms, WCF and WWF. .NET Core (2016–2019) was the open-source, cross-platform, high-performance rewrite. .NET 5 (2020) onward unifies them as simply ".NET", released every November with LTS versions (6, 8, 10) supported for 3 years.
- New projects use .NET 8/10.
In short: Managed code runs under the CLR, which handles memory (GC), type safety and exceptions — C#, VB.NET, F#.
- Unmanaged code runs directly on the OS without the CLR — C/C++ DLLs, Win32 APIs, COM components — and manages its own memory. .NET calls unmanaged code via P/Invoke or COM interop;
- resources from it must be released with
IDisposable/SafeHandle.
In short: The unit of deployment and versioning in .NET — a .dll or .exe containing IL code, metadata describing all types and members, a manifest (name, version, culture, public key token, references) and optional resources.
- Assemblies can be private (in the app folder), shared in the GAC (.NET Framework) or satellite (localised resources).
In short: The GC allocates objects by bumping a pointer and, when the gen-0 budget is used up, suspends threads, marks all objects reachable from roots (stack, statics, handles), compacts survivors and updates references.
- Generations: gen 0 for new objects (collected often and cheaply), gen 1 as a buffer, gen 2 for long-lived objects (full, expensive collections).
- Large objects (≥ 85,000 bytes) go to the LOH, collected with gen 2 and not compacted by default.
In short: Dispose (from IDisposable) is called deterministically by your code (using) to release resources early;
- a finalizer (
~Class()) is called non-deterministically by the GC's finalizer thread as a safety net for unmanaged resources and makes objects survive an extra GC. - The Dispose pattern:
Dispose()callsDispose(true)andGC.SuppressFinalize(this); - the finalizer calls
Dispose(false); Dispose(bool disposing)frees managed resources only whendisposingis true and unmanaged resources always.- Prefer
SafeHandleover writing finalizers.
In short: Reference-type objects are always on the managed heap; variables hold references to them.
- Value types are stored inline where they are declared: on the stack (or in registers) for locals, inside the containing object for fields, inside the array for array elements, and on the heap when boxed or captured by closures/async methods.
- So "value types live on the stack" is only partly true.