Overview
The Base Class Library, usually called the BCL, is the core set of .NET types that C# developers use every day. It includes fundamental types for values, text, collections, dates and times, I/O, exceptions, threading, asynchronous programming, reflection, memory handling, and many other common tasks.
In C#, many language keywords are aliases for BCL types. For example, int maps to System.Int32, string maps to System.String, object maps to System.Object, and bool maps to System.Boolean. This means that learning C# well also requires understanding the common .NET types behind the language syntax.
This topic matters because interviewers often use BCL types to test practical .NET knowledge. They may ask why string is immutable, when to use StringBuilder, how List<T> differs from IEnumerable<T>, why DateTimeOffset is often safer than DateTime, how Task works with async/await, or why decimal is preferred for money. These questions reveal whether a candidate can write correct, maintainable, and production-ready C# code.
Common BCL types are used everywhere in .NET applications: ASP.NET Core APIs, background services, EF Core applications, file processing, logging, validation, data transfer objects, domain models, middleware, tests, and cloud integrations. A strong understanding of these types helps developers avoid common bugs involving nulls, culture-sensitive string comparisons, time zones, collection performance, memory allocations, and asynchronous execution.
Core Concepts
What the BCL Means in .NET
The Base Class Library is the common library of reusable types provided by .NET. It contains the basic building blocks used by applications and higher-level frameworks.
Examples include:
System.ObjectSystem.StringSystem.Int32System.DateTimeSystem.DateTimeOffsetSystem.GuidSystem.ExceptionSystem.Collections.Generic.List<T>System.Collections.Generic.Dictionary<TKey, TValue>System.Threading.Tasks.TaskSystem.IO.Stream
In interviews, the term BCL is often used broadly to mean the standard .NET types developers are expected to know. Strictly speaking, the BCL is the foundation layer, while higher-level framework libraries build on top of it.
A practical way to think about it:
int count = 10; // System.Int32
string name = "Alice"; // System.String
object value = count; // System.Object, with boxing
DateTimeOffset now = DateTimeOffset.UtcNow;
List<string> names = new(); // System.Collections.Generic.List<T>
C# Keywords vs .NET Type Names
C# has aliases for many BCL types. The alias and the full .NET type name represent the same type.
int a = 10;
System.Int32 b = 20;
string first = "Alice";
System.String second = "Bob";
object obj = first;
System.Object sameObj = second;
Common aliases include:
Best practice is to use C# aliases in normal C# code for readability, especially for primitive types. Use full type names when discussing APIs, reflection, documentation, or when consistency with framework type names matters.
System.Object
System.Object is the root type for most C# types. Classes, structs, arrays, delegates, enums, and records ultimately derive from object, either directly or indirectly.
Important members include:
ToString()Equals(object?)GetHashCode()GetType()
Example:
object value = "hello";
Console.WriteLine(value.ToString()); // hello
Console.WriteLine(value.GetType().Name); // String
For value types, assigning to object causes boxing.
int number = 42;
object boxed = number; // boxing
int unboxed = (int)boxed; // unboxing
Boxing creates an object wrapper for a value type. It can add allocations and should be avoided in performance-sensitive paths.
Common interview point: object gives flexibility, but generic types usually provide better type safety and performance.
// Less type-safe; may require casting.
ArrayList oldList = new();
oldList.Add(123);
oldList.Add("abc");
// Better: strongly typed and avoids boxing for int.
List<int> numbers = new();
numbers.Add(123);
Value Types and Reference Types
.NET types are generally categorized as value types or reference types.
Value types store their actual value directly in the variable location. Examples include:
intbooldecimalDateTimeDateTimeOffsetGuidTimeSpanstructtypesenumtypes
Reference types store a reference to an object. Examples include:
stringobject- arrays
- classes
- delegates
- most collection types such as
List<T>andDictionary<TKey, TValue>
Example:
int x = 10;
int y = x;
y = 20;
Console.WriteLine(x); // 10
var list1 = new List<int> { 1, 2 };
var list2 = list1;
list2.Add(3);
Console.WriteLine(list1.Count); // 3
A common mistake is saying value types always live on the stack and reference types always live on the heap. That is an oversimplification. The important interview answer is about value semantics versus reference semantics, not only memory location.
Numeric Types
C# numeric types are BCL value types. The most common are int, long, double, and decimal.
Use int for general whole numbers unless the range requires long.
int pageSize = 50;
long fileSizeInBytes = 5_000_000_000;
Use double for scientific, measurement, or approximate floating-point calculations.
double temperature = 36.6;
double distance = 123.45;
Use decimal for money and financial calculations where base-10 precision matters.
decimal price = 19.99m;
decimal taxRate = 0.08m;
decimal total = price + (price * taxRate);
Common mistakes:
- Using
doublefor money. - Comparing floating-point values with exact equality.
- Ignoring overflow.
- Mixing numeric types without understanding implicit and explicit conversions.
Example of floating-point precision issue:
double result = 0.1 + 0.2;
Console.WriteLine(result == 0.3); // Often false
For approximate values, compare using a tolerance:
bool AreClose(double a, double b, double tolerance = 0.000001)
{
return Math.Abs(a - b) < tolerance;
}
For money, prefer decimal:
decimal result = 0.1m + 0.2m;
Console.WriteLine(result == 0.3m); // true
bool and Boolean Logic
bool maps to System.Boolean and represents true or false.
bool isActive = true;
if (isActive)
{
Console.WriteLine("User is active");
}
A nullable Boolean, bool?, can represent true, false, or null.
bool? isApproved = null;
if (isApproved == true)
{
Console.WriteLine("Approved");
}
else if (isApproved == false)
{
Console.WriteLine("Rejected");
}
else
{
Console.WriteLine("Pending");
}
Use bool? only when the missing or unknown state has real business meaning. Otherwise, prefer non-nullable bool.
char, Unicode, and Text
char maps to System.Char. It represents a UTF-16 code unit, not necessarily a complete user-perceived character.
char letter = 'A';
This matters because some Unicode characters require more than one UTF-16 code unit. Interviewers may ask this to test whether you understand that string.Length counts char values, not always user-visible characters.
string text = "😀";
Console.WriteLine(text.Length); // May be 2 because the emoji uses a surrogate pair
For normal business applications, char is useful for simple character-level checks. For advanced Unicode handling, use APIs designed for Unicode text processing.
string and System.String
string is an alias for System.String. It represents text and is immutable. Once created, a string instance cannot be changed. Operations such as Replace, Substring, ToUpper, and concatenation return new strings.
string name = "alice";
string upper = name.ToUpperInvariant();
Console.WriteLine(name); // alice
Console.WriteLine(upper); // ALICE
String immutability makes strings safe to share and helps with predictable behavior, but repeated modifications can allocate many temporary strings.
// Inefficient for many iterations
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString();
}
For repeated string building, use StringBuilder.
var builder = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
builder.Append(i);
}
string result = builder.ToString();
Important best practices:
- Use
string.IsNullOrEmptyorstring.IsNullOrWhiteSpacefor validation. - Use
StringComparison.OrdinalorStringComparison.OrdinalIgnoreCasefor non-linguistic comparisons such as IDs, keys, and tokens. - Use culture-aware comparison only when comparing text for users.
- Use interpolation for readable formatting.
- Avoid unnecessary
ToLower()orToUpper()before comparison.
Example:
bool matches = string.Equals(
input,
expected,
StringComparison.OrdinalIgnoreCase);
StringBuilder
StringBuilder is used to efficiently build strings through repeated modifications.
Use it when:
- Building large strings in loops.
- Appending many fragments conditionally.
- Creating generated text, logs, reports, SQL fragments, or export files.
var sb = new StringBuilder();
sb.AppendLine("Report");
sb.AppendLine("------");
foreach (var item in items)
{
sb.AppendLine($"- {item.Name}: {item.Value}");
}
string report = sb.ToString();
Do not use StringBuilder for simple one-line interpolation. This is usually less readable and unnecessary.
// Good
string message = $"Hello {firstName} {lastName}";
Nullable Value Types: Nullable<T> and T?
A nullable value type represents all values of a value type plus null.
int? age = null;
DateTime? completedAt = null;
decimal? discount = 10.5m;
int? is shorthand for Nullable<int>.
Nullable<int> a = 10;
int? b = 20;
Common members:
HasValueValueGetValueOrDefault()
int? score = null;
int safeScore = score ?? 0;
if (score.HasValue)
{
Console.WriteLine(score.Value);
}
Best practices:
- Use nullable value types when data can be missing, such as optional database fields.
- Prefer
??or pattern matching instead of directly accessing.Value. - Avoid nullable value types when a default value is meaningful and enough.
Common mistake:
int? score = null;
Console.WriteLine(score.Value); // Throws InvalidOperationException
Better:
if (score is int actualScore)
{
Console.WriteLine(actualScore);
}
Nullable Reference Types
Nullable reference types are a C# feature that helps express whether a reference is expected to be null.
string name = "Alice"; // should not be null
string? middleName = null; // can be null
This is a compile-time analysis feature. It does not create a different runtime type. Both string and string? are still System.String at runtime.
Use nullable reference types to make APIs clearer:
public sealed class Customer
{
public required string Name { get; init; }
public string? Email { get; init; }
}
Common interview points:
T?for value types meansNullable<T>.string?for reference types means the compiler should treat the value as possibly null.- Nullable reference types help prevent
NullReferenceException, but they do not replace runtime validation.
DateTime, DateTimeOffset, DateOnly, TimeOnly, and TimeSpan
Date and time types are common sources of production bugs.
DateTime represents a date and time. It has a Kind property that can be Local, Utc, or Unspecified.
DateTime createdAt = DateTime.UtcNow;
DateTimeOffset represents a date and time with an offset from UTC. It is often better for logging, events, audit fields, and distributed systems because it identifies a more precise point in time.
DateTimeOffset createdAt = DateTimeOffset.UtcNow;
DateOnly represents a calendar date without a time.
DateOnly birthDate = new(1995, 5, 20);
TimeOnly represents a time of day without a date.
TimeOnly openingTime = new(9, 0);
TimeSpan represents a duration or interval.
TimeSpan timeout = TimeSpan.FromSeconds(30);
Best practices:
- Use
DateTimeOffsetfor timestamps in distributed applications. - Use
DateOnlyfor dates such as birth dates, due dates, or schedule dates when time is irrelevant. - Use
TimeOnlyfor time-of-day values such as opening hours. - Use
TimeSpanfor durations, timeouts, and elapsed time. - Store server-side timestamps in UTC unless there is a strong reason not to.
- Do not use
DateTime.Nowfor cross-system timestamps.
Example:
public sealed class Order
{
public Guid Id { get; init; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public DateOnly RequiredDeliveryDate { get; init; }
}
Guid
Guid represents a globally unique identifier. It is commonly used for IDs in APIs, databases, distributed systems, and correlation tracking.
Guid id = Guid.NewGuid();
Use cases:
- Entity identifiers.
- Public resource IDs.
- Correlation IDs.
- Idempotency keys.
- Distributed message identifiers.
Trade-offs:
- Good for distributed uniqueness.
- Larger than
intorlong. - Random GUIDs can affect clustered database index performance if used as primary keys without planning.
- Not naturally ordered unless using ordered/sequential GUID strategies.
Example:
public sealed record CreateOrderRequest(Guid CustomerId, decimal Amount);
Common mistake: treating GUIDs as secure secrets. A GUID is an identifier, not an authorization mechanism.
Uri
Uri represents a Uniform Resource Identifier. It is useful for URLs and resource addresses.
var uri = new Uri("https://example.com/api/customers?page=1");
Console.WriteLine(uri.Host); // example.com
Console.WriteLine(uri.Scheme); // https
Console.WriteLine(uri.Query); // ?page=1
Use Uri instead of manual string parsing when working with URLs.
Common use cases:
- API clients.
- File/resource addresses.
- Redirect URLs.
- Validation of absolute and relative URLs.
Best practice: still validate business rules. A syntactically valid URI is not automatically safe or allowed.
Arrays
Arrays are fixed-size collections of elements of the same type.
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[0]);
Arrays are useful when:
- The size is known.
- You need fast index-based access.
- You are interoperating with APIs that expect arrays.
- You are working with buffers.
Trade-offs:
- Fixed length after creation.
- Less flexible than
List<T>for add/remove operations. - Good performance for simple indexed data.
byte[] buffer = new byte[4096];
List<T>
List<T> is a resizable generic collection.
var names = new List<string>();
names.Add("Alice");
names.Add("Bob");
Use List<T> when:
- You need ordered items.
- You need index access.
- You need to add or remove items dynamically.
- You want a concrete in-memory collection.
Common mistakes:
- Returning
List<T>from every API whenIReadOnlyList<T>orIEnumerable<T>would express intent better. - Modifying a list while enumerating it.
- Assuming
List<T>is thread-safe.
Example of safer API design:
public IReadOnlyList<string> GetRoles()
{
return new List<string> { "Admin", "User" };
}
Dictionary<TKey, TValue>
Dictionary<TKey, TValue> stores key/value pairs and provides fast lookup by key.
var usersById = new Dictionary<Guid, string>();
Guid userId = Guid.NewGuid();
usersById[userId] = "Alice";
if (usersById.TryGetValue(userId, out string? name))
{
Console.WriteLine(name);
}
Use Dictionary<TKey, TValue> when:
- You need lookup by unique key.
- You want to avoid repeated linear searches.
- You need a map from one value to another.
Best practices:
- Use
TryGetValuewhen a key may not exist. - Choose a suitable comparer for string keys.
- Do not depend on dictionary ordering unless the specific API guarantees it.
- Ensure keys are immutable or at least not mutated in ways that affect equality or hash code.
Example using a case-insensitive string comparer:
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Content-Type"] = "application/json"
};
HashSet<T>
HashSet<T> stores unique values and provides fast membership checks.
var allowedRoles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Admin",
"Manager"
};
if (allowedRoles.Contains(userRole))
{
Console.WriteLine("Allowed");
}
Use it when:
- You need uniqueness.
- You frequently check whether a value exists.
- You need set operations such as union, intersection, or difference.
var selectedIds = new HashSet<int> { 1, 2, 3 };
var activeIds = new HashSet<int> { 2, 3, 4 };
selectedIds.IntersectWith(activeIds); // selectedIds now contains 2 and 3
Queue<T> and Stack<T>
Queue<T> is first-in, first-out.
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
Console.WriteLine(queue.Dequeue()); // first
Stack<T> is last-in, first-out.
var stack = new Stack<string>();
stack.Push("first");
stack.Push("second");
Console.WriteLine(stack.Pop()); // second
Use cases:
Queue<T>: work queues, breadth-first traversal, ordered processing.Stack<T>: undo operations, depth-first traversal, parsing, backtracking.
For multi-threaded producer/consumer scenarios, use types from System.Collections.Concurrent instead of manually locking generic collections unless you have a specific reason.
Collection Interfaces: IEnumerable<T>, ICollection<T>, IList<T>, and IReadOnlyList<T>
Collection interfaces express what operations a caller can perform.
Example:
public decimal CalculateTotal(IEnumerable<OrderLine> lines)
{
return lines.Sum(line => line.Price * line.Quantity);
}
This method accepts many sources: arrays, lists, query results, and generated sequences.
For returning data:
public IReadOnlyList<CustomerDto> GetCustomers()
{
return customers
.Select(c => new CustomerDto(c.Id, c.Name))
.ToList();
}
Best practice:
- Accept the least specific type needed.
- Return a type that communicates whether the result is materialized and whether callers can mutate it.
- Avoid exposing mutable internal collections directly.
LINQ, IEnumerable<T>, and Deferred Execution
LINQ works heavily with IEnumerable<T> and extension methods.
var activeUsers = users
.Where(user => user.IsActive)
.OrderBy(user => user.Name)
.Select(user => user.Name);
Many LINQ operations use deferred execution, meaning the query is not executed until enumerated.
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number); // 2, 3, 4
}
Use ToList() or ToArray() to materialize results when needed.
var activeUsers = users
.Where(user => user.IsActive)
.ToList();
Common mistakes:
- Enumerating expensive queries multiple times.
- Returning deferred queries after the underlying context or resource has been disposed.
- Using LINQ where a simple loop would be clearer or faster.
- Confusing
IEnumerable<T>withIQueryable<T>in database queries.
IQueryable<T> vs IEnumerable<T>
IEnumerable<T> represents in-memory or enumerable data. LINQ operations run in .NET code.
IQueryable<T> represents a query that can be translated by a provider, such as EF Core translating expression trees to SQL.
IQueryable<Customer> query = dbContext.Customers
.Where(c => c.IsActive);
List<Customer> result = await query.ToListAsync();
Important interview point: applying filters before materialization can allow the database to do the work.
// Better: filter in the database
var customers = await dbContext.Customers
.Where(c => c.IsActive)
.ToListAsync();
// Worse for large tables: loads too much data first
var allCustomers = await dbContext.Customers.ToListAsync();
var activeCustomers = allCustomers.Where(c => c.IsActive).ToList();
Tuples and ValueTuple
Tuples are useful for grouping a small number of values without creating a dedicated type.
(string Name, int Age) person = ("Alice", 30);
Console.WriteLine(person.Name);
Console.WriteLine(person.Age);
They are often used for private helper methods or simple returns.
private static (bool IsValid, string? Error) ValidateAge(int age)
{
if (age < 0)
{
return (false, "Age cannot be negative.");
}
return (true, null);
}
Best practices:
- Use tuples for small, local, obvious groupings.
- Use records or classes for public APIs, domain models, or values with business meaning.
- Name tuple elements for readability.
Common mistake:
// Hard to understand
(int, string, bool) result = GetData();
Better:
(int Count, string Message, bool IsSuccessful) result = GetData();
KeyValuePair<TKey, TValue>
KeyValuePair<TKey, TValue> represents a key/value pair, commonly seen when enumerating dictionaries.
foreach (KeyValuePair<string, int> item in countsByName)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
Modern C# also allows deconstruction in many cases:
foreach (var (name, count) in countsByName)
{
Console.WriteLine($"{name}: {count}");
}
Use KeyValuePair<TKey, TValue> when working with dictionary-like APIs. For richer business concepts, prefer a named type.
Enum
Enums represent a named set of constant values.
public enum OrderStatus
{
Pending,
Paid,
Shipped,
Cancelled
}
Use enums when a value must be one of a known set of options.
public sealed class Order
{
public OrderStatus Status { get; set; }
}
Best practices:
- Use clear names.
- Define explicit values when persisted to a database or exchanged through an API.
- Be careful when parsing external input.
- Consider unknown or future values in distributed systems.
public enum OrderStatus
{
Unknown = 0,
Pending = 1,
Paid = 2,
Shipped = 3,
Cancelled = 4
}
Parsing safely:
if (Enum.TryParse<OrderStatus>(input, ignoreCase: true, out var status))
{
Console.WriteLine(status);
}
Exception and Common Exception Types
System.Exception is the base type for exceptions in .NET.
Common exception types include:
ArgumentExceptionArgumentNullExceptionArgumentOutOfRangeExceptionInvalidOperationExceptionNotSupportedExceptionKeyNotFoundExceptionFormatExceptionTimeoutExceptionOperationCanceledException
Example:
public void SetPageSize(int pageSize)
{
if (pageSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be greater than zero.");
}
}
Best practices:
- Use exceptions for exceptional or invalid situations, not normal control flow.
- Throw the most specific exception type that matches the problem.
- Include useful messages.
- Preserve stack traces by using
throw;instead ofthrow ex;. - Avoid swallowing exceptions silently.
try
{
ProcessOrder(order);
}
catch (ValidationException ex)
{
logger.LogWarning(ex, "Order validation failed.");
throw;
}
Task, Task<T>, ValueTask<T>, and CancellationToken
Task and Task<T> represent asynchronous operations. They are central to async and await.
public async Task<CustomerDto> GetCustomerAsync(Guid id, CancellationToken cancellationToken)
{
Customer customer = await repository.GetByIdAsync(id, cancellationToken);
return new CustomerDto(customer.Id, customer.Name);
}
Use Task when an asynchronous operation does not return a value.
public async Task SaveAsync(Order order, CancellationToken cancellationToken)
{
await repository.SaveAsync(order, cancellationToken);
}
Use Task<T> when it returns a value.
public async Task<int> CountAsync(CancellationToken cancellationToken)
{
return await repository.CountAsync(cancellationToken);
}
CancellationToken allows cooperative cancellation.
public async Task<string> DownloadAsync(HttpClient client, string url, CancellationToken cancellationToken)
{
return await client.GetStringAsync(url, cancellationToken);
}
ValueTask<T> can reduce allocations in specialized high-performance scenarios where the result is often available synchronously. For normal application code, prefer Task<T> unless profiling shows a reason to use ValueTask<T>.
Common mistakes:
- Blocking async code with
.Resultor.Wait(). - Forgetting to pass
CancellationTokenthrough layers. - Using
async voidexcept for event handlers. - Wrapping I/O-bound async code in
Task.Rununnecessarily.
Stream, File, Directory, and Path
Stream represents a sequence of bytes. Many I/O APIs use streams because they can work with files, memory, network data, compression, and other sources.
await using FileStream stream = File.OpenRead("data.txt");
using var reader = new StreamReader(stream);
string content = await reader.ReadToEndAsync();
Common stream types:
FileStreamMemoryStreamNetworkStreamBufferedStreamCryptoStreamGZipStream
File provides static helpers for file operations.
string text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("output.txt", text);
Directory provides helpers for directories.
Directory.CreateDirectory("exports");
Path helps combine and inspect file paths safely.
string fullPath = Path.Combine(baseDirectory, "exports", "report.txt");
string extension = Path.GetExtension(fullPath);
Best practices:
- Prefer
Path.Combineover manual string concatenation. - Dispose streams with
usingorawait using. - Use async file APIs for scalable I/O operations.
- Do not trust file paths from users without validation.
IDisposable and IAsyncDisposable
IDisposable is used by types that need deterministic cleanup, such as streams, database contexts, timers, and unmanaged resource wrappers.
using var stream = File.OpenRead("data.txt");
The using statement ensures Dispose() is called even if an exception occurs.
IAsyncDisposable supports asynchronous cleanup.
await using var stream = File.OpenRead("data.txt");
Common interview point: garbage collection manages memory, but IDisposable handles non-memory resources or resources that should be released promptly.
Span<T>, ReadOnlySpan<T>, Memory<T>, and ReadOnlyMemory<T>
Span<T> and ReadOnlySpan<T> represent contiguous regions of memory without copying. They are useful for high-performance parsing, slicing, and buffer manipulation.
string value = "ABC-123";
ReadOnlySpan<char> span = value.AsSpan();
ReadOnlySpan<char> prefix = span[..3];
ReadOnlySpan<char> number = span[4..];
Console.WriteLine(prefix.ToString()); // ABC
Console.WriteLine(number.ToString()); // 123
Span<T> is stack-only and cannot be stored in fields of normal classes, captured by lambdas, or used across await boundaries. Memory<T> and ReadOnlyMemory<T> can be stored and used in asynchronous APIs.
Use these types when:
- Avoiding allocations in hot paths.
- Parsing large text or binary data.
- Working with buffers.
- Building performance-sensitive libraries.
For normal business code, simpler types such as string, arrays, and List<T> are often more readable.
Regex
Regex is used for pattern matching in text.
bool isValid = Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
Use regex for patterns, not for simple string checks.
// Prefer this for simple checks
bool hasPrefix = value.StartsWith("ORD-", StringComparison.Ordinal);
Best practices:
- Keep regex patterns readable.
- Use timeouts for untrusted input in security-sensitive code.
- Avoid overly complex regex when parsing with normal code would be clearer.
Math, Random, and Utility Types
Math provides common numeric operations.
double rounded = Math.Round(12.345, 2);
int max = Math.Max(10, 20);
Random generates pseudo-random numbers.
int value = Random.Shared.Next(1, 101);
For security-sensitive random values, use cryptographic random APIs instead of Random.
byte[] bytes = RandomNumberGenerator.GetBytes(32);
Common use cases:
Math: calculations, rounding, min/max, absolute values.Random: non-security simulations, test data, simple randomized behavior.- Cryptographic random APIs: tokens, secrets, keys, security-sensitive identifiers.
Equality, Comparers, and Hash Codes
Many BCL types rely on equality and comparison.
Important types include:
IEquatable<T>IEqualityComparer<T>EqualityComparer<T>.DefaultIComparable<T>IComparer<T>StringComparer
Example using StringComparer:
var users = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["alice"] = 1
};
Console.WriteLine(users.ContainsKey("ALICE")); // true
When implementing equality for custom types used in dictionaries or sets, ensure Equals and GetHashCode are consistent.
public sealed class ProductCode : IEquatable<ProductCode>
{
public ProductCode(string value)
{
Value = value;
}
public string Value { get; }
public bool Equals(ProductCode? other)
{
return other is not null &&
string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object? obj) => Equals(obj as ProductCode);
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
}
}
Common mistake: overriding Equals but not GetHashCode, causing incorrect behavior in Dictionary<TKey, TValue> or HashSet<T>.
Type, Attribute, and Reflection Basics
System.Type represents metadata about a type at runtime. Reflection lets code inspect assemblies, types, properties, methods, and attributes.
Type type = typeof(Customer);
Console.WriteLine(type.Name);
Console.WriteLine(type.FullName);
Attributes add metadata to code elements.
[Obsolete("Use NewMethod instead.")]
public void OldMethod()
{
}
Common use cases:
- Dependency injection scanning.
- Serialization.
- Validation frameworks.
- Test frameworks.
- ASP.NET Core attributes.
- Mapping libraries.
Trade-offs:
- Reflection is powerful and flexible.
- It can be slower and less type-safe than direct code.
- It should be used carefully in hot paths.
Practical Type Selection Guide
Common Mistakes with BCL Types
Common mistakes include:
- Using
doublefor money. - Using
DateTime.Nowfor distributed timestamps. - Ignoring
DateTime.Kind. - Using
DateTimewhenDateOnlyorTimeOnlybetter expresses intent. - Building large strings with repeated
+=in loops. - Comparing strings without specifying
StringComparison. - Exposing mutable
List<T>properties directly. - Enumerating
IEnumerable<T>multiple times when it represents an expensive query. - Using
.Resultor.Wait()on async operations. - Forgetting to dispose
Stream,DbContext, or other disposable resources. - Overusing
objectinstead of generics. - Ignoring culture when parsing or formatting user-facing values.
- Treating GUIDs as secrets.
- Using regex for simple string operations.
Best Practices for Interview Answers
Strong interview answers should explain both what the type is and why you would choose it.
For example, a weak answer is:
Use Dictionary because it is faster.
A stronger answer is:
Use Dictionary<TKey, TValue> when you need fast lookup by key and each key is unique. It is usually more appropriate than scanning a List<T> repeatedly. However, the key type must have stable equality and hash-code behavior, and you should use a suitable comparer for string keys.
When discussing BCL types, mention:
- Correct type semantics.
- Performance characteristics.
- Mutability.
- Nullability.
- Thread-safety.
- Culture and globalization concerns.
- Resource management.
- Real production scenarios.