.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.
Questions 1-20 of 100
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
asyncandawait, 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.
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.
Runtime Base class library SDK Loads intermediate language, compiles it to machine code and manages memory Collections, input and output, networking, JSON, threading, cryptography and more Compiler, build system, test runner and the dotnetcommand-line toolIncludes the just-in-time compiler and the garbage collector Shared by all application types, from console tools to web services Needed on build machines, not on servers that only run applications Installed on servers, bundled into self-contained apps or replaced by native compilation Versioned together with the runtime Can 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- The C# compiler turns source code into intermediate language inside assemblies.
- 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.
- 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.
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 types Reference types struct,enum, numbers,bool,DateTimeclass,record, arrays,string, delegatesAssignment copies the whole value Assignment copies a reference to the same object Usually stored inline, in locals or inside other objects Stored on the managed heap and collected by the garbage collector Best for small, immutable data, such as a money amount Best for entities with identity and larger data Practical consequences:
- Changing a copy of a struct does not change the original, which surprises people who expect object-like behavior.
- Large structs are expensive to copy; keep structs small or pass them with
inorref. - Storing a struct in an
objector a non-generic interface boxes it, allocating a heap copy; generics avoid boxing. stringis a reference type but behaves like a value because it is immutable.- 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; }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.
Class Record Struct Reference type compared by reference Reference type, or record struct, compared by valueValue type compared by value when you implement it Mutable by default Immutable by default with positional properties Should be small and usually immutable Best for services, entities and objects with behavior Best for messages, events, results and request data Best for small values such as coordinates or money What records add:
- Value equality: two records with the same property values are equal.
withexpressions: create a modified copy without changing the original.- Readable
ToStringthat prints the property values. - Concise positional syntax:
record Order(int Id, decimal Total);.
Cautions:
- A record containing a list compares the list by reference, not by contents.
- Records used as dictionary keys must not change after insertion.
- 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);Q5.How do nullable reference types work in C#?
With nullable reference types enabled, the compiler tracks whether each reference may be null.
stringmeans "should never be null" andstring?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 knowsnameis not null.
Useful tools:
- Null-conditional and coalescing operators:
customer?.Address?.City ?? "unknown". ArgumentNullException.ThrowIfNull(arg)at public boundaries.- Attributes such as
[NotNullWhen(true)]on methods likeTryGetValue, which teach the compiler about your own patterns. requiredmembers, 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);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;.
requiredmembers: marking a propertyrequiredmakes the compiler insist that every object initializer sets it, which removes long constructors while still preventing half-initialized objects.The
fieldkeyword: inside an accessor,fieldrefers to the compiler-generated backing field, so you can add validation without declaring the field yourself.Guidelines:
- Prefer
initfor data objects, such as requests and configuration. - Validate in setters or constructors, so invalid states cannot exist.
- Keep property getters cheap; expensive work belongs in methods.
Example
requiredforces initialization,initblocks later changes and a setter validates with thefieldkeyword.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.Skuafter creation would not compile, because the setter isinit.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
isexpressions,switchstatements andswitchexpressions, 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:
- Business rules read like a table of cases.
- The compiler warns when a
switchexpression over an enum or closed set of types misses a case. - Nulls are handled explicitly with
nullandnot nullpatterns.
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
switchexpression.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);- Type patterns:
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>orTask<T>. Unlike erased generics in some languages, .NET keeps type arguments at run time and generates specialized code for value types, soList<int>stores integers without boxing.Constraints restrict which types are allowed and unlock members:
where T : classorwhere T : structfor 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 : notnullto forbid nullable types.- Generic math:
where T : INumber<T>allows arithmetic on any numeric type.
Variance:
outandinon interface type parameters, as inIEnumerable<out T>, let anIEnumerable<string>be used where anIEnumerable<object>is expected.Guidelines:
- Use generics to remove duplication across types, especially for collections, results and repositories.
- Keep constraints minimal, so the type remains broadly usable.
- Avoid deep generic hierarchies that make error messages and code hard to follow.
Example
One
Summethod 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; }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:
- LINQ queries, which take lambdas as filters and projections.
- Middleware pipelines, built from chains of delegates.
- Callbacks and strategies passed into services.
Q10.How does LINQ work, and what is deferred execution?
LINQ, language-integrated query, adds query operators such as
Where,Select,GroupByandOrderByto any sequence. Most operators are deferred: building a query only describes the work, and nothing runs until the sequence is enumerated, for example withforeach,ToListorCount.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,Anyand other aggregations.Good practices:
- Materialize with
ToListwhen a result is used several times. - Prefer
Any()overCount() > 0to stop at the first match. - Keep queries readable; split long chains into well-named steps.
- 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); // 3Q11.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 application Operators take expression trees that a provider translates Every element is loaded, then filtered in memory Filters, sorting and paging run in the database Any C# method can be used in a filter Only expressions the provider can translate are allowed Best for collections already in memory Best for querying large data stores efficiently Common mistakes:
- Switching to
IEnumerabletoo early: callingAsEnumerableorToListbeforeWhereloads the whole table and filters it in the application. - Untranslatable expressions: calling custom methods inside a database query either fails at run time or forces client-side evaluation.
- Exposing
IQueryablefrom repositories, which lets any caller build arbitrary, possibly slow, queries.
Guideline: compose filters, sorting and paging on
IQueryable, then materialize the final, small result withToListAsyncor similar.- Switching to
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:
- Throw specific exceptions with clear messages, such as
ArgumentExceptionfor bad arguments, and custom types for domain failures that callers catch. - Catch only what you can handle, and let the rest reach a central handler that logs it and returns a 500 response.
- Preserve the stack trace: use
throw;to rethrow, neverthrow ex;, and wrap with an inner exception when adding context. - Use exception filters,
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound), to catch narrowly without unwinding for other cases. - 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
TryParseinstead of catchingFormatException.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; }- Throw specific exceptions with clear messages, such as
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, andDisposereleases them immediately rather than waiting for the garbage collector. Ausingstatement or declaration guaranteesDisposeruns 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 usingcallsDisposeAsyncfor types implementingIAsyncDisposable, which can flush data without blocking a thread.
Rules:
- Dispose what you create; do not dispose objects owned by someone else, such as instances given to you by the dependency injection container.
- Dispose in reverse order of creation, which
usingdeclarations do automatically. - Never use an object after disposing it; well-written types throw
ObjectDisposedException. - Implement
IDisposableon your own types when they own disposable fields.
Common misuse: wrapping a shared
HttpClientinusingfor 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}"); }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 collection Server collection One heap, tuned for low memory and responsiveness One heap and collector thread per core Default for desktop apps Default for web services, with higher throughput Uses less memory Uses more memory Reducing pressure in services:
- Avoid allocations on hot paths, such as repeated string building and LINQ in tight loops.
- Pool large buffers instead of allocating them per request.
- Watch generation 2 collections and pause times in metrics.
- In containers, the runtime respects memory limits; dynamic adaptation modes can shrink heap counts for services with modest load.
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>andReadOnlySpan<T>live only on the stack: they cannot be stored in fields of classes, captured by lambdas or used acrossawait.Memory<T>andReadOnlyMemory<T>can be stored and used in async code, and produce a span when needed.stackalloccreates small temporary buffers on the stack as spans.
Where they help:
- Parsing text, such as splitting a line into fields with
IndexOfand slicing instead ofstring.Split. - Binary protocols and serialization.
- 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.50Q16.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:
StringBuilderappends into a growing buffer and produces one string at the end. - Joining collections:
string.Joinis simple and fast. - Hot paths: interpolated string handlers let APIs such as
TryWriteand logging format directly into buffers without allocating intermediate strings.
Comparison and culture:
- Use
StringComparison.OrdinalorOrdinalIgnoreCasefor identifiers, keys and protocol text; culture-aware comparison is slower and can give surprising results. - Format numbers and dates with
CultureInfo.InvariantCulturein 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.
- A few pieces:
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
thismodifier. 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)toQueryExtensions.Paginate(items, 2, 20). The extension can only use the public members of the extended type; it adds no state.Good uses:
- Fluent helpers on interfaces, such as LINQ itself, which is built from extension methods on
IEnumerable<T>. - Registration helpers that group related setup, such as
services.AddOrderServices(). - 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
objectorstringthat clutter every autocomplete list.
Recent C# versions extend the idea with extension blocks, which can also add properties and static members.
- Fluent helpers on interfaces, such as LINQ itself, which is built from extension methods on
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
initproperties for immutable data objects. readonlyfields andreadonly structto prevent modification.- Immutable collections, such as
ImmutableList<T>andImmutableDictionary<TKey, TValue>, which return a new collection on every change while sharing unchanged parts internally. - Frozen collections,
FrozenDictionaryandFrozenSet, 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.Exchangeor avolatilefield when it changes. Readers always see a consistent version.Trade-offs:
- Immutable collections are slower to modify than mutable ones; use them where reads and sharing dominate.
- Building many intermediate versions creates garbage; use builders for bulk changes.
- Immutability is shallow: an immutable record holding a mutable list is still mutable through the list.
- Records and
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
EqualsandGetHashCodethat agree: equal objects must return the same hash code.Implementing value equality on a class:
- Implement
IEquatable<T>with a typedEquals. - Override
Equals(object)andGetHashCode, combining the same fields, for example withHashCode.Combine. - Override
==and!=for consistency. - 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; }- Implement
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 access Values looked up by key Unique items with fast membership checks Items processed first in, first out Fast to append; slow to search by value Constant-time lookup on average Constant-time Containson averageFast enqueue and dequeue Best for results and ordered data Best for indexes and caches Best for deduplication and "seen" checks Best for work waiting to be processed Also useful:
SortedDictionaryandSortedSetkeep 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
ListandDictionarywhen the size is known, pass a comparer such asStringComparer.OrdinalIgnoreCasefor case-insensitive keys, and expose read-only interfaces from public APIs.