DEV_NET_CORE
GET_STARTED
.NETC# Language Foundations

Common BCL Types in C#

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.Object
  • System.String
  • System.Int32
  • System.DateTime
  • System.DateTimeOffset
  • System.Guid
  • System.Exception
  • System.Collections.Generic.List<T>
  • System.Collections.Generic.Dictionary<TKey, TValue>
  • System.Threading.Tasks.Task
  • System.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:

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

Code
int a = 10;
System.Int32 b = 20;

string first = "Alice";
System.String second = "Bob";

object obj = first;
System.Object sameObj = second;

Common aliases include:

C# Alias.NET TypeCommon Use
objectSystem.ObjectBase type of almost all C# types
stringSystem.StringText
boolSystem.BooleanTrue/false values
byteSystem.ByteBinary data and small unsigned values
shortSystem.Int16Small integer values
intSystem.Int32Default integer type
longSystem.Int64Large integer values
floatSystem.Single32-bit floating-point number
doubleSystem.Double64-bit floating-point number
decimalSystem.DecimalFinancial and high-precision decimal values
charSystem.CharUTF-16 code unit

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:

Code
object value = "hello";

Console.WriteLine(value.ToString());     // hello
Console.WriteLine(value.GetType().Name); // String

For value types, assigning to object causes boxing.

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

Code
// 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:

  • int
  • bool
  • decimal
  • DateTime
  • DateTimeOffset
  • Guid
  • TimeSpan
  • struct types
  • enum types

Reference types store a reference to an object. Examples include:

  • string
  • object
  • arrays
  • classes
  • delegates
  • most collection types such as List<T> and Dictionary<TKey, TValue>

Example:

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

Code
int pageSize = 50;
long fileSizeInBytes = 5_000_000_000;

Use double for scientific, measurement, or approximate floating-point calculations.

Code
double temperature = 36.6;
double distance = 123.45;

Use decimal for money and financial calculations where base-10 precision matters.

Code
decimal price = 19.99m;
decimal taxRate = 0.08m;
decimal total = price + (price * taxRate);

Common mistakes:

  • Using double for 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:

Code
double result = 0.1 + 0.2;
Console.WriteLine(result == 0.3); // Often false

For approximate values, compare using a tolerance:

Code
bool AreClose(double a, double b, double tolerance = 0.000001)
{
    return Math.Abs(a - b) < tolerance;
}

For money, prefer decimal:

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

Code
bool isActive = true;

if (isActive)
{
    Console.WriteLine("User is active");
}

A nullable Boolean, bool?, can represent true, false, or null.

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

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

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

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

Code
// Inefficient for many iterations
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString();
}

For repeated string building, use StringBuilder.

Code
var builder = new StringBuilder();

for (int i = 0; i < 1000; i++)
{
    builder.Append(i);
}

string result = builder.ToString();

Important best practices:

  • Use string.IsNullOrEmpty or string.IsNullOrWhiteSpace for validation.
  • Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase for 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() or ToUpper() before comparison.

Example:

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

Code
// 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.

Code
int? age = null;
DateTime? completedAt = null;
decimal? discount = 10.5m;

int? is shorthand for Nullable<int>.

Code
Nullable<int> a = 10;
int? b = 20;

Common members:

  • HasValue
  • Value
  • GetValueOrDefault()
Code
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:

Code
int? score = null;
Console.WriteLine(score.Value); // Throws InvalidOperationException

Better:

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

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

Code
public sealed class Customer
{
    public required string Name { get; init; }
    public string? Email { get; init; }
}

Common interview points:

  • T? for value types means Nullable<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.

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

Code
DateTimeOffset createdAt = DateTimeOffset.UtcNow;

DateOnly represents a calendar date without a time.

Code
DateOnly birthDate = new(1995, 5, 20);

TimeOnly represents a time of day without a date.

Code
TimeOnly openingTime = new(9, 0);

TimeSpan represents a duration or interval.

Code
TimeSpan timeout = TimeSpan.FromSeconds(30);

Best practices:

  • Use DateTimeOffset for timestamps in distributed applications.
  • Use DateOnly for dates such as birth dates, due dates, or schedule dates when time is irrelevant.
  • Use TimeOnly for time-of-day values such as opening hours.
  • Use TimeSpan for durations, timeouts, and elapsed time.
  • Store server-side timestamps in UTC unless there is a strong reason not to.
  • Do not use DateTime.Now for cross-system timestamps.

Example:

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

Code
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 int or long.
  • 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:

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

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

Code
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.
Code
byte[] buffer = new byte[4096];

List<T>

List<T> is a resizable generic collection.

Code
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 when IReadOnlyList<T> or IEnumerable<T> would express intent better.
  • Modifying a list while enumerating it.
  • Assuming List<T> is thread-safe.

Example of safer API design:

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

Code
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 TryGetValue when 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:

Code
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
    ["Content-Type"] = "application/json"
};

HashSet<T>

HashSet<T> stores unique values and provides fast membership checks.

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

Code
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");

Console.WriteLine(queue.Dequeue()); // first

Stack<T> is last-in, first-out.

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

TypeMeaningCommon Use
IEnumerable<T>Can be enumeratedInput sequence, streaming, LINQ
ICollection<T>Can add/remove/countMutable collection abstraction
IList<T>Indexed mutable listList-like abstraction with indexing
IReadOnlyCollection<T>Can enumerate and countRead-only collection result
IReadOnlyList<T>Read-only indexed listRead-only ordered result
List<T>Concrete resizable listInternal implementation or when concrete features are needed

Example:

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

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

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

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

Code
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> with IQueryable<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.

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

Code
// 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.

Code
(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.

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

Code
// Hard to understand
(int, string, bool) result = GetData();

Better:

Code
(int Count, string Message, bool IsSuccessful) result = GetData();

KeyValuePair<TKey, TValue>

KeyValuePair<TKey, TValue> represents a key/value pair, commonly seen when enumerating dictionaries.

Code
foreach (KeyValuePair<string, int> item in countsByName)
{
    Console.WriteLine($"{item.Key}: {item.Value}");
}

Modern C# also allows deconstruction in many cases:

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

Code
public enum OrderStatus
{
    Pending,
    Paid,
    Shipped,
    Cancelled
}

Use enums when a value must be one of a known set of options.

Code
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.
Code
public enum OrderStatus
{
    Unknown = 0,
    Pending = 1,
    Paid = 2,
    Shipped = 3,
    Cancelled = 4
}

Parsing safely:

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

  • ArgumentException
  • ArgumentNullException
  • ArgumentOutOfRangeException
  • InvalidOperationException
  • NotSupportedException
  • KeyNotFoundException
  • FormatException
  • TimeoutException
  • OperationCanceledException

Example:

Code
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 of throw ex;.
  • Avoid swallowing exceptions silently.
Code
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.

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

Code
public async Task SaveAsync(Order order, CancellationToken cancellationToken)
{
    await repository.SaveAsync(order, cancellationToken);
}

Use Task<T> when it returns a value.

Code
public async Task<int> CountAsync(CancellationToken cancellationToken)
{
    return await repository.CountAsync(cancellationToken);
}

CancellationToken allows cooperative cancellation.

Code
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 .Result or .Wait().
  • Forgetting to pass CancellationToken through layers.
  • Using async void except for event handlers.
  • Wrapping I/O-bound async code in Task.Run unnecessarily.

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.

Code
await using FileStream stream = File.OpenRead("data.txt");
using var reader = new StreamReader(stream);

string content = await reader.ReadToEndAsync();

Common stream types:

  • FileStream
  • MemoryStream
  • NetworkStream
  • BufferedStream
  • CryptoStream
  • GZipStream

File provides static helpers for file operations.

Code
string text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("output.txt", text);

Directory provides helpers for directories.

Code
Directory.CreateDirectory("exports");

Path helps combine and inspect file paths safely.

Code
string fullPath = Path.Combine(baseDirectory, "exports", "report.txt");
string extension = Path.GetExtension(fullPath);

Best practices:

  • Prefer Path.Combine over manual string concatenation.
  • Dispose streams with using or await 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.

Code
using var stream = File.OpenRead("data.txt");

The using statement ensures Dispose() is called even if an exception occurs.

IAsyncDisposable supports asynchronous cleanup.

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

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

Code
bool isValid = Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");

Use regex for patterns, not for simple string checks.

Code
// 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.

Code
double rounded = Math.Round(12.345, 2);
int max = Math.Max(10, 20);

Random generates pseudo-random numbers.

Code
int value = Random.Shared.Next(1, 101);

For security-sensitive random values, use cryptographic random APIs instead of Random.

Code
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>.Default
  • IComparable<T>
  • IComparer<T>
  • StringComparer

Example using StringComparer:

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

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

Code
Type type = typeof(Customer);

Console.WriteLine(type.Name);
Console.WriteLine(type.FullName);

Attributes add metadata to code elements.

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

ScenarioPrefer
General whole numberint
Large whole numberlong
Moneydecimal
Approximate scientific/measurement valuedouble
Textstring
Repeated text buildingStringBuilder
Unique distributed IDGuid
Timestamp in distributed systemsDateTimeOffset
Date without timeDateOnly
Time without dateTimeOnly
Duration or timeoutTimeSpan
Ordered resizable listList<T>
Unique valuesHashSet<T>
Key/value lookupDictionary<TKey, TValue>
Sequence inputIEnumerable<T>
Read-only ordered resultIReadOnlyList<T>
Async operationTask or Task<T>
CancellationCancellationToken
Binary or file dataStream
Resource cleanupIDisposable or IAsyncDisposable
High-performance memory sliceSpan<T> or ReadOnlySpan<T>

Common Mistakes with BCL Types

Common mistakes include:

  • Using double for money.
  • Using DateTime.Now for distributed timestamps.
  • Ignoring DateTime.Kind.
  • Using DateTime when DateOnly or TimeOnly better 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 .Result or .Wait() on async operations.
  • Forgetting to dispose Stream, DbContext, or other disposable resources.
  • Overusing object instead 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:

Code
Use Dictionary because it is faster.

A stronger answer is:

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

Interview Practice

PreviousCollection Choices in C#Next UpExceptions