Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

.NET Backend Developer Top 100 Interview Questions

100 .NET backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable C# code. Covers the language and runtime, async and concurrency, the web pipeline, data access, caching, security, testing, diagnostics, performance and system design, from the basics to production incidents.

100 questions

Questions 1-20 of 100

  1. Q1.What does a .NET backend developer own beyond writing API endpoints?

    A .NET backend developer owns services from design to production: their APIs, data access, performance, reliability and operations. Endpoints are the visible part; most of the work is making them correct under concurrency, fast enough and easy to run.

    Typical responsibilities:

    • API design: clear contracts, validation, versioning and consistent error responses.
    • Async and concurrency: non-blocking code with async and await, cancellation and safe shared state.
    • Data access: queries, transactions, connection pooling and caching.
    • Dependency injection and configuration: composing services with the right lifetimes and validated settings.
    • Reliability: timeouts, retries, health checks and graceful shutdown.
    • Performance: profiling, reducing allocations and garbage collection pressure.
    • Testing: unit, integration and contract tests in continuous integration.
    • Observability: structured logs, metrics and distributed traces.
    • Delivery: reproducible builds, small container images and safe deployments.

    The modern .NET platform is cross-platform and fast, so the skill lies less in the framework and more in using its async model, memory model and tooling well.

  2. Q2.How do the .NET runtime, the base class library and the SDK fit together?

    .NET has three layers. The runtime executes compiled code, the base class library provides the standard types every program uses, and the SDK contains the tools that build, test and publish applications.

    RuntimeBase class librarySDK
    Loads intermediate language, compiles it to machine code and manages memoryCollections, input and output, networking, JSON, threading, cryptography and moreCompiler, build system, test runner and the dotnet command-line tool
    Includes the just-in-time compiler and the garbage collectorShared by all application types, from console tools to web servicesNeeded on build machines, not on servers that only run applications
    Installed on servers, bundled into self-contained apps or replaced by native compilationVersioned together with the runtimeCan target older runtime versions

    How code runs:

    flowchart LR
      A["C# source"] --> B["Compiler"]
      B --> C["Assembly with intermediate language"]
      C --> D["Just-in-time compiler at run time"]
      C --> E["Ahead-of-time compiler at build time"]
      D --> F["Machine code"]
      E --> F
    1. The C# compiler turns source code into intermediate language inside assemblies.
    2. At run time, the just-in-time compiler turns methods into machine code when they are first called, and recompiles hot methods with better optimizations.
    3. Ahead-of-time options can produce native code at build time for faster startup.

    Deployment choices: framework-dependent apps rely on an installed runtime, while self-contained apps carry their own, which simplifies servers at the cost of size.

  3. Q3.How do value types and reference types differ in C#?

    A value type holds its data directly, and assigning it copies the data. A reference type holds a reference to an object on the heap, and assigning it copies the reference, so both variables point to the same object.

    Value typesReference types
    struct, enum, numbers, bool, DateTimeclass, record, arrays, string, delegates
    Assignment copies the whole valueAssignment copies a reference to the same object
    Usually stored inline, in locals or inside other objectsStored on the managed heap and collected by the garbage collector
    Best for small, immutable data, such as a money amountBest for entities with identity and larger data

    Practical consequences:

    1. Changing a copy of a struct does not change the original, which surprises people who expect object-like behavior.
    2. Large structs are expensive to copy; keep structs small or pass them with in or ref.
    3. Storing a struct in an object or a non-generic interface boxes it, allocating a heap copy; generics avoid boxing.
    4. string is a reference type but behaves like a value because it is immutable.
    5. Nullable value types, such as int?, add a flag for "no value".

    Example

    Copying a struct copies its data; copying a class instance shares the object.

    var a = new PointStruct(1, 2);
    var b = a;
    b.X = 10;
    Console.WriteLine($"{a.X} {b.X}"); // 1 10
    
    var c = new PointClass { X = 1, Y = 2 };
    var d = c;
    d.X = 10;
    Console.WriteLine($"{c.X} {d.X}"); // 10 10
    
    struct PointStruct(int x, int y)
    {
        public int X = x;
        public int Y = y;
    }
    
    class PointClass
    {
        public int X;
        public int Y;
    }
  4. Q4.When do you use a class, a record or a struct in C#?

    Choose by identity, mutability and size. Classes model entities with identity and behavior, records model immutable data compared by value, and structs model small values where avoiding heap allocation matters.

    ClassRecordStruct
    Reference type compared by referenceReference type, or record struct, compared by valueValue type compared by value when you implement it
    Mutable by defaultImmutable by default with positional propertiesShould be small and usually immutable
    Best for services, entities and objects with behaviorBest for messages, events, results and request dataBest for small values such as coordinates or money

    What records add:

    • Value equality: two records with the same property values are equal.
    • with expressions: create a modified copy without changing the original.
    • Readable ToString that prints the property values.
    • Concise positional syntax: record Order(int Id, decimal Total);.

    Cautions:

    1. A record containing a list compares the list by reference, not by contents.
    2. Records used as dictionary keys must not change after insertion.
    3. Structs larger than a few fields cost more to copy than they save.

    Example

    Records compare by value and support non-destructive copies with with.

    var first = new Order(7, 1250m);
    var same = new Order(7, 1250m);
    var discounted = first with { Total = 1000m };
    
    Console.WriteLine(first == same); // True
    Console.WriteLine(discounted);    // Order { Id = 7, Total = 1000 }
    Console.WriteLine(first.Total);   // 1250
    
    record Order(int Id, decimal Total);
  5. Q5.How do nullable reference types work in C#?

    With nullable reference types enabled, the compiler tracks whether each reference may be null. string means "should never be null" and string? means "may be null". The compiler warns when code might dereference a possibly null value or assign null to a non-nullable reference.

    How it helps:

    • Many null reference exceptions become compile-time warnings.
    • Method signatures document whether callers may pass or receive null.
    • Flow analysis understands null checks, so after if (name is null) return; the compiler knows name is not null.

    Useful tools:

    1. Null-conditional and coalescing operators: customer?.Address?.City ?? "unknown".
    2. ArgumentNullException.ThrowIfNull(arg) at public boundaries.
    3. Attributes such as [NotNullWhen(true)] on methods like TryGetValue, which teach the compiler about your own patterns.
    4. required members, so object initializers must set important properties.

    Limits: the checks are compile-time only. Data from JSON, databases or older libraries can still contain nulls at run time, so validate at those boundaries, and treat warnings as errors in builds to keep the annotations honest.

    Example

    Flow analysis knows the value is not null after the check, and the coalescing operator supplies a default.

    Console.WriteLine(Describe(new Customer("Asha", null)));            // Asha (no city)
    Console.WriteLine(Describe(new Customer("Ravi", new Address("Pune")))); // Ravi (Pune)
    
    static string Describe(Customer customer)
    {
        string city = customer.Address?.City ?? "no city";
        return $"{customer.Name} ({city})";
    }
    
    record Address(string City);
    record Customer(string Name, Address? Address);
  6. Q6.How do properties, init-only setters and required members work in C#?

    Properties expose data through accessors, so a class can validate or compute values while callers use simple syntax. Modern C# adds controls for when properties can be set and which ones must be set.

    Accessor options:

    • get; set; allows changes at any time.
    • get; init; allows setting only during object creation, through a constructor or an object initializer, which gives immutability with readable initializers.
    • get; private set; allows changes only inside the class.
    • Computed properties with only a getter, such as public decimal Total => Price * Quantity;.

    required members: marking a property required makes the compiler insist that every object initializer sets it, which removes long constructors while still preventing half-initialized objects.

    The field keyword: inside an accessor, field refers to the compiler-generated backing field, so you can add validation without declaring the field yourself.

    Guidelines:

    1. Prefer init for data objects, such as requests and configuration.
    2. Validate in setters or constructors, so invalid states cannot exist.
    3. Keep property getters cheap; expensive work belongs in methods.

    Example

    required forces initialization, init blocks later changes and a setter validates with the field keyword.

    var item = new LineItem { Sku = "LAMP-1", Quantity = 3, Price = 450m };
    Console.WriteLine(item.Total); // 1350
    
    try
    {
        item.Price = -5m;
    }
    catch (ArgumentOutOfRangeException error)
    {
        Console.WriteLine(error.ParamName); // value
    }
    
    class LineItem
    {
        public required string Sku { get; init; }
        public required int Quantity { get; init; }
    
        public decimal Price
        {
            get => field;
            set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
        }
    
        public decimal Total => Price * Quantity;
    }

    Assigning item.Sku after creation would not compile, because the setter is init.

  7. Q7.How does pattern matching work in modern C#?

    Pattern matching tests a value against a shape and extracts parts of it in one step. It works in is expressions, switch statements and switch expressions, and it replaces long chains of type checks and casts with clear, declarative rules.

    Kinds of patterns:

    • Type patterns: if (shape is Circle c) checks the type and binds a variable.
    • Property patterns: order is { Status: "paid", Total: > 1000 }.
    • Relational and logical patterns: < 0, >= 18 and < 65, not null.
    • Positional patterns for records and tuples: (0, 0).
    • List patterns: [first, .., last] matches sequences by position.

    Why it helps:

    1. Business rules read like a table of cases.
    2. The compiler warns when a switch expression over an enum or closed set of types misses a case.
    3. Nulls are handled explicitly with null and not null patterns.

    Order matters: cases are evaluated from top to bottom, so put the most specific ones first, and end with a discard _ for everything else.

    Example

    A shipping rule uses property, relational and list patterns in one switch expression.

    Console.WriteLine(Shipping(new Order("IN", 450m, ["book"])));          // 50
    Console.WriteLine(Shipping(new Order("IN", 1200m, ["lamp", "desk"]))); // 0
    Console.WriteLine(Shipping(new Order("SG", 300m, [])));                // empty order
    Console.WriteLine(Shipping(new Order("SG", 300m, ["pen"])));           // 400
    
    static string Shipping(Order order) => order switch
    {
        { Items: [] } => "empty order",
        { Country: "IN", Total: >= 999 } => "0",
        { Country: "IN" } => "50",
        _ => "400",
    };
    
    record Order(string Country, decimal Total, string[] Items);
  8. Q8.How do generics and generic constraints work in C#?

    Generics let types and methods work with any type while staying type-safe, such as List<T> or Task<T>. Unlike erased generics in some languages, .NET keeps type arguments at run time and generates specialized code for value types, so List<int> stores integers without boxing.

    Constraints restrict which types are allowed and unlock members:

    • where T : class or where T : struct for reference or value types.
    • where T : new() for types with a parameterless constructor.
    • where T : IComparable<T> or any interface or base class, to call its members.
    • where T : notnull to forbid nullable types.
    • Generic math: where T : INumber<T> allows arithmetic on any numeric type.

    Variance: out and in on interface type parameters, as in IEnumerable<out T>, let an IEnumerable<string> be used where an IEnumerable<object> is expected.

    Guidelines:

    1. Use generics to remove duplication across types, especially for collections, results and repositories.
    2. Keep constraints minimal, so the type remains broadly usable.
    3. Avoid deep generic hierarchies that make error messages and code hard to follow.

    Example

    One Sum method works for integers and decimals through a generic math constraint.

    using System.Numerics;
    
    Console.WriteLine(Sum([1, 2, 3]));        // 6
    Console.WriteLine(Sum([12.50m, 7.25m]));  // 19.75
    Console.WriteLine(Largest(["pear", "apple", "fig"])); // pear
    
    static T Sum<T>(T[] values) where T : INumber<T>
    {
        T total = T.Zero;
        foreach (var v in values) total += v;
        return total;
    }
    
    static T Largest<T>(T[] values) where T : IComparable<T>
    {
        T best = values[0];
        foreach (var v in values)
            if (v.CompareTo(best) > 0) best = v;
        return best;
    }
  9. Q9.How do delegates, lambdas and events relate in C#?

    A delegate is a type-safe reference to a method. A lambda is a concise way to write a method inline, which can be converted to a delegate. An event is a delegate field that outside code may subscribe to and unsubscribe from, but only the owning class may invoke.

    Common delegate types:

    • Func<T, TResult> for functions that return a value.
    • Action<T> for functions that return nothing.
    • Predicate<T> for tests that return a Boolean.

    Lambdas and closures: a lambda can capture variables from its surrounding scope. Captured variables are shared, not copied, so changes after the lambda is created are visible inside it, and long-lived lambdas keep captured objects alive.

    Events and memory leaks: subscribing a short-lived object to an event on a long-lived object keeps the short-lived one alive, because the publisher holds a delegate referencing it. Always unsubscribe, or design with weak references or explicit lifetimes.

    Where they appear in backend code:

    1. LINQ queries, which take lambdas as filters and projections.
    2. Middleware pipelines, built from chains of delegates.
    3. Callbacks and strategies passed into services.
  10. Q10.How does LINQ work, and what is deferred execution?

    LINQ, language-integrated query, adds query operators such as Where, Select, GroupBy and OrderBy to any sequence. Most operators are deferred: building a query only describes the work, and nothing runs until the sequence is enumerated, for example with foreach, ToList or Count.

    Consequences of deferred execution:

    • Multiple enumeration: enumerating the same query twice runs it twice, which can repeat expensive work or database calls.
    • Late evaluation: changes to the source collection before enumeration are reflected in the results.
    • Captured variables are read at enumeration time, not when the query is written.

    Operators that execute immediately: ToList, ToArray, ToDictionary, Count, First, Sum, Any and other aggregations.

    Good practices:

    1. Materialize with ToList when a result is used several times.
    2. Prefer Any() over Count() > 0 to stop at the first match.
    3. Keep queries readable; split long chains into well-named steps.
    4. In hot paths, measure, since LINQ adds allocations and delegate calls compared with plain loops.

    Interview tip: interviewers often show a query enumerated twice and ask what happens; mentioning that each enumeration runs the whole query again, including database calls, is the answer they want.

    Example

    The query runs only when enumerated, so a value added afterwards appears in the results, and the filter runs again on each enumeration.

    var prices = new List<int> { 100, 450, 1200 };
    int evaluations = 0;
    
    var expensive = prices.Where(p => { evaluations++; return p > 400; });
    prices.Add(2000);
    
    Console.WriteLine(string.Join(",", expensive)); // 450,1200,2000
    Console.WriteLine(expensive.Count());           // 3
    Console.WriteLine(evaluations);                 // 8
    
    var snapshot = expensive.ToList();
    prices.Add(5000);
    Console.WriteLine(snapshot.Count);              // 3
  11. Q11.What is the difference between `IEnumerable<T>` and `IQueryable<T>`?

    IEnumerable<T> runs LINQ operators in memory, as C# code over objects. IQueryable<T> builds an expression tree that a provider, such as a database mapper, translates into another language, such as SQL, and runs at the source.

    IEnumerable<T>IQueryable<T>
    Operators take delegates and run in the applicationOperators take expression trees that a provider translates
    Every element is loaded, then filtered in memoryFilters, sorting and paging run in the database
    Any C# method can be used in a filterOnly expressions the provider can translate are allowed
    Best for collections already in memoryBest for querying large data stores efficiently

    Common mistakes:

    1. Switching to IEnumerable too early: calling AsEnumerable or ToList before Where loads the whole table and filters it in the application.
    2. Untranslatable expressions: calling custom methods inside a database query either fails at run time or forces client-side evaluation.
    3. Exposing IQueryable from repositories, which lets any caller build arbitrary, possibly slow, queries.

    Guideline: compose filters, sorting and paging on IQueryable, then materialize the final, small result with ToListAsync or similar.

  12. Q12.How should a .NET backend use exceptions?

    Use exceptions for unexpected failures and broken contracts, not for normal control flow. Expected outcomes, such as "not found" or "validation failed", are often clearer as return values, while exceptions handle problems the immediate caller cannot reasonably deal with.

    Good practices:

    1. Throw specific exceptions with clear messages, such as ArgumentException for bad arguments, and custom types for domain failures that callers catch.
    2. Catch only what you can handle, and let the rest reach a central handler that logs it and returns a 500 response.
    3. Preserve the stack trace: use throw; to rethrow, never throw ex;, and wrap with an inner exception when adding context.
    4. Use exception filters, catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound), to catch narrowly without unwinding for other cases.
    5. Never swallow exceptions silently; at minimum, log them.

    Performance: throwing is expensive compared with returning a value, so avoid exceptions on hot paths for expected cases, such as using TryParse instead of catching FormatException.

    At API boundaries, map known exceptions to consistent error responses, and never expose stack traces to clients.

    Example

    An exception filter handles only the not-found case, and the original exception is kept as the inner exception.

    Console.WriteLine(Load(1));  // found
    Console.WriteLine(Load(2));  // not found
    try
    {
        Load(3);
    }
    catch (InvalidOperationException e)
    {
        Console.WriteLine($"{e.Message} <- {e.InnerException?.Message}"); // load failed <- timeout
    }
    
    static string Load(int id)
    {
        try
        {
            return Fetch(id);
        }
        catch (StoreException e) when (e.Code == 404)
        {
            return "not found";
        }
        catch (StoreException e)
        {
            throw new InvalidOperationException("load failed", e);
        }
    }
    
    static string Fetch(int id) => id switch
    {
        1 => "found",
        2 => throw new StoreException(404, "missing"),
        _ => throw new StoreException(504, "timeout"),
    };
    
    class StoreException(int code, string message) : Exception(message)
    {
        public int Code { get; } = code;
    }
  13. Q13.How do `using`, `IDisposable` and `IAsyncDisposable` manage resources?

    Objects that hold unmanaged or scarce resources, such as file handles, sockets or database connections, implement IDisposable, and Dispose releases them immediately rather than waiting for the garbage collector. A using statement or declaration guarantees Dispose runs when the scope ends, even if an exception is thrown.

    Forms:

    • using var stream = File.OpenRead(path); disposes at the end of the enclosing block.
    • using (...) { } disposes at the end of the explicit block.
    • await using calls DisposeAsync for types implementing IAsyncDisposable, which can flush data without blocking a thread.

    Rules:

    1. Dispose what you create; do not dispose objects owned by someone else, such as instances given to you by the dependency injection container.
    2. Dispose in reverse order of creation, which using declarations do automatically.
    3. Never use an object after disposing it; well-written types throw ObjectDisposedException.
    4. Implement IDisposable on your own types when they own disposable fields.

    Common misuse: wrapping a shared HttpClient in using for every request, which disposes its connection pool and can exhaust sockets.

    Example

    Resources are disposed in reverse order when the scope ends, even when an exception escapes.

    try
    {
        using var first = new Resource("connection");
        using var second = new Resource("file");
        Console.WriteLine("working");
        throw new InvalidOperationException("failed");
    }
    catch (InvalidOperationException e)
    {
        Console.WriteLine($"caught: {e.Message}");
    }
    // Output:
    // working
    // disposed file
    // disposed connection
    // caught: failed
    
    class Resource(string name) : IDisposable
    {
        public void Dispose() => Console.WriteLine($"disposed {name}");
    }
  14. Q14.How does garbage collection work in .NET?

    The .NET garbage collector allocates objects in a managed heap and periodically frees those that are no longer reachable. It is generational: most objects die young, so collecting the youngest generation often is cheap, while older objects are collected rarely.

    Generations:

    flowchart LR
      A["New allocation"] --> B["Generation 0"]
      B -->|"Survives a collection"| C["Generation 1"]
      C -->|"Survives again"| D["Generation 2"]
      E["Object of about 85 KB or more"] --> F["Large object heap"]
    • Generation 0: new objects; collected frequently and quickly.
    • Generation 1: a buffer between short- and long-lived objects.
    • Generation 2: long-lived objects, such as caches; full collections are more expensive.
    • Large object heap: objects of about 85 KB or more go here directly and are collected with generation 2.
    Workstation collectionServer collection
    One heap, tuned for low memory and responsivenessOne heap and collector thread per core
    Default for desktop appsDefault for web services, with higher throughput
    Uses less memoryUses more memory

    Reducing pressure in services:

    1. Avoid allocations on hot paths, such as repeated string building and LINQ in tight loops.
    2. Pool large buffers instead of allocating them per request.
    3. Watch generation 2 collections and pause times in metrics.
    4. In containers, the runtime respects memory limits; dynamic adaptation modes can shrink heap counts for services with modest load.
  15. Q15.What are `Span<T>` and `Memory<T>`, and why do they help performance?

    Span<T> is a lightweight view over a contiguous region of memory, such as part of an array, a string or a stack buffer, without copying it. Slicing a span costs nothing, so parsing and processing code can work on pieces of data without allocating substrings or arrays.

    Key properties:

    • Span<T> and ReadOnlySpan<T> live only on the stack: they cannot be stored in fields of classes, captured by lambdas or used across await.
    • Memory<T> and ReadOnlyMemory<T> can be stored and used in async code, and produce a span when needed.
    • stackalloc creates small temporary buffers on the stack as spans.

    Where they help:

    1. Parsing text, such as splitting a line into fields with IndexOf and slicing instead of string.Split.
    2. Binary protocols and serialization.
    3. Formatting numbers and dates directly into buffers with TryFormat.

    When not to bother: ordinary business code where allocations are not a measured problem. Spans make code more complex, so use them in hot paths shown by profiling.

    Example

    Fields are read by slicing a span, so no substrings or arrays are allocated.

    ReadOnlySpan<char> line = "ORD-1042,3,1250.50";
    
    int first = line.IndexOf(',');
    ReadOnlySpan<char> id = line[..first];
    ReadOnlySpan<char> rest = line[(first + 1)..];
    int second = rest.IndexOf(',');
    
    int quantity = int.Parse(rest[..second]);
    decimal price = decimal.Parse(rest[(second + 1)..], System.Globalization.CultureInfo.InvariantCulture);
    
    Console.WriteLine(id.ToString());       // ORD-1042
    Console.WriteLine(quantity * price);    // 3751.50
  16. Q16.How do strings, `StringBuilder` and interpolation affect performance in C#?

    Strings are immutable, so every concatenation creates a new string. That is fine for a few pieces, but building a string in a loop with + copies the growing text again and again, which wastes processor time and memory.

    Choosing a technique:

    • A few pieces: + or interpolation, $"{name}: {total}", is clear and efficient.
    • Loops and large text: StringBuilder appends into a growing buffer and produces one string at the end.
    • Joining collections: string.Join is simple and fast.
    • Hot paths: interpolated string handlers let APIs such as TryWrite and logging format directly into buffers without allocating intermediate strings.

    Comparison and culture:

    1. Use StringComparison.Ordinal or OrdinalIgnoreCase for identifiers, keys and protocol text; culture-aware comparison is slower and can give surprising results.
    2. Format numbers and dates with CultureInfo.InvariantCulture in machine-readable output, such as logs and files.

    Interning and memory: the runtime stores one copy of each string literal; strings created at run time are not deduplicated, so large numbers of repeated values, such as status names, can be cached or represented with enums.

  17. Q17.How do extension methods work, and when should you use them?

    An extension method is a static method in a static class whose first parameter has the this modifier. Callers use it as if it were an instance method of that type, which lets you add helpers to types you do not own, such as interfaces from the base class library.

    How they work: the compiler rewrites items.Paginate(2, 20) to QueryExtensions.Paginate(items, 2, 20). The extension can only use the public members of the extended type; it adds no state.

    Good uses:

    1. Fluent helpers on interfaces, such as LINQ itself, which is built from extension methods on IEnumerable<T>.
    2. Registration helpers that group related setup, such as services.AddOrderServices().
    3. Domain conveniences, such as money.ToDisplayString().

    When to avoid them:

    • Adding behavior that belongs in the type itself, when you own the type.
    • Hiding expensive work behind something that looks like a cheap property.
    • Generic extensions on object or string that clutter every autocomplete list.

    Recent C# versions extend the idea with extension blocks, which can also add properties and static members.

  18. Q18.How do immutable data and immutable collections help in concurrent code?

    An object that cannot change after creation can be shared freely between threads without locks, because no thread can observe it half-updated. Immutability removes a whole class of race conditions and makes code easier to reason about.

    Tools in .NET:

    • Records and init properties for immutable data objects.
    • readonly fields and readonly struct to prevent modification.
    • Immutable collections, such as ImmutableList<T> and ImmutableDictionary<TKey, TValue>, which return a new collection on every change while sharing unchanged parts internally.
    • Frozen collections, FrozenDictionary and FrozenSet, which are built once and optimized for very fast reads.

    A common pattern: keep configuration, routing tables or reference data in an immutable snapshot, and replace the whole snapshot atomically with Interlocked.Exchange or a volatile field when it changes. Readers always see a consistent version.

    Trade-offs:

    1. Immutable collections are slower to modify than mutable ones; use them where reads and sharing dominate.
    2. Building many intermediate versions creates garbage; use builders for bulk changes.
    3. Immutability is shallow: an immutable record holding a mutable list is still mutable through the list.
  19. Q19.How do equality and hashing work for custom types in .NET?

    By default, classes use reference equality: two variables are equal only if they point to the same object. Records and structs use value equality: they are equal when their fields are equal. Any type used as a dictionary key or set member must have Equals and GetHashCode that agree: equal objects must return the same hash code.

    Implementing value equality on a class:

    1. Implement IEquatable<T> with a typed Equals.
    2. Override Equals(object) and GetHashCode, combining the same fields, for example with HashCode.Combine.
    3. Override == and != for consistency.
    4. Base equality only on fields that do not change while the object is in a collection.

    Records generate all of this automatically, which is a major reason to use them for data.

    Common pitfalls:

    • Mutating a key after inserting it into a dictionary makes it impossible to find.
    • Comparing strings with culture-sensitive rules when ordinal comparison was intended.
    • Using floating-point fields in equality, where tiny rounding differences break matches.
    • Forgetting that a record's collection fields compare by reference.

    Example

    A class without overrides compares by reference, while a record with the same data compares by value and works as a dictionary key.

    var dict = new Dictionary<SkuRecord, int> { [new SkuRecord("LAMP", "blue")] = 5 };
    Console.WriteLine(dict[new SkuRecord("LAMP", "blue")]); // 5
    
    var a = new SkuClass("LAMP", "blue");
    var b = new SkuClass("LAMP", "blue");
    Console.WriteLine(a.Equals(b)); // False
    
    var set = new HashSet<SkuRecord> { new("LAMP", "blue"), new("LAMP", "blue"), new("LAMP", "red") };
    Console.WriteLine(set.Count); // 2
    
    record SkuRecord(string Code, string Color);
    
    class SkuClass(string code, string color)
    {
        public string Code { get; } = code;
        public string Color { get; } = color;
    }
  20. Q20.How do you choose among the common collection types in .NET?

    Choose by how the collection is used: ordered access, lookup by key, membership checks or processing in arrival order. The right type often matters more for performance than any micro-optimization.

    List<T>Dictionary<TKey, TValue>HashSet<T>Queue<T>
    Ordered items with fast index accessValues looked up by keyUnique items with fast membership checksItems processed first in, first out
    Fast to append; slow to search by valueConstant-time lookup on averageConstant-time Contains on averageFast enqueue and dequeue
    Best for results and ordered dataBest for indexes and cachesBest for deduplication and "seen" checksBest for work waiting to be processed

    Also useful:

    • SortedDictionary and SortedSet keep keys ordered, at a logarithmic cost per operation.
    • PriorityQueue<TElement, TPriority> returns the smallest priority first, for scheduling.
    • Concurrent collections, such as ConcurrentDictionary, for sharing across threads.
    • Frozen collections for read-only lookups built once at startup.

    Practical tips: give capacities to List and Dictionary when the size is known, pass a comparer such as StringComparer.OrdinalIgnoreCase for case-insensitive keys, and expose read-only interfaces from public APIs.