Generics & LINQ
Generics
Generics let you write type-safe, reusable code without knowing the concrete type at compile time.
// Without generics โ not type-safe, requires casting
public class Box
{
private object _value;
public void Set(object val) { _value = val; }
public object Get() { return _value; }
}
// With generics โ type-safe, no casting needed
public class Box<T>
{
private T _value;
public void Set(T val) { _value = val; }
public T Get() { return _value; }
}
var intBox = new Box<int>();
var stringBox = new Box<string>();
intBox.Set(42);
Generic Constraints
// T must implement IComparable<T>
public T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
// T must be a class (reference type) with a parameterless constructor
public T CreateInstance<T>() where T : class, new()
=> new T();
// T must be a value type (struct)
public void Process<T>(T val) where T : struct { ... }
// T must implement an interface
public void Save<T>(T entity) where T : IEntity { ... }
Common Generic Interfaces
IEnumerable<T>, IList<T>, ICollection<T>, IDictionary<TKey, TValue>, IReadOnlyList<T> โ know when to use each.
LINQ
LINQ (Language Integrated Query) lets you query collections using a consistent, readable syntax. Two styles exist โ method syntax and query syntax โ and they're equivalent.
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Method syntax (more common in practice)
var evenSquares = numbers
.Where(n => n % 2 == 0)
.Select(n => n * n)
.OrderByDescending(n => n)
.ToList();
// Query syntax (looks like SQL)
var evenSquares2 = (from n in numbers
where n % 2 == 0
select n * n)
.OrderByDescending(n => n)
.ToList();
Deferred Execution โ Critical to Understand
LINQ queries are not executed when you write them. They're executed when you enumerate the results (call .ToList(), .FirstOrDefault(), foreach, etc.).
var query = numbers.Where(n => n > 5); // โ Not executed yet โ just defines the query
// Execution happens here:
var result1 = query.ToList(); // Execute now, materialize to list
var result2 = query.FirstOrDefault(); // Execute now, get first match
foreach (var n in query) { ... } // Execute per iteration
// This means the query runs again each time you enumerate it!
// If you need the results multiple times, call .ToList() once and cache it.
IEnumerable<T> vs IQueryable<T>
// IEnumerable<T> โ LINQ to Objects โ executes in memory
List<Order> orders = GetAllOrders(); // All rows loaded into memory
var recent = orders.Where(o => o.Date > cutoff); // Filtered in C#
// IQueryable<T> โ LINQ to SQL / EF Core โ translates to SQL
IQueryable<Order> query = _context.Orders; // No SQL yet
var recent = query.Where(o => o.Date > cutoff); // Adds WHERE clause to SQL
var result = recent.ToList(); // SQL executed: SELECT * FROM Orders WHERE Date > @p0
The gotcha: Adding .AsEnumerable() or .ToList() mid-query forces in-memory execution from that point. Any LINQ after that runs in C#, not SQL.
// โ Loads ALL orders to memory, then filters in C#
var expensive = _context.Orders
.ToList() // โ Everything loaded here
.Where(o => o.Total > 1000);
// โ
Filters in SQL โ only matching rows returned
var expensive = _context.Orders
.Where(o => o.Total > 1000)
.ToList();
Common LINQ Methods
var orders = _context.Orders.AsQueryable();
// Filtering
orders.Where(o => o.Status == "Active")
// Projection
orders.Select(o => new { o.OrderID, o.TotalAmount })
orders.Select(o => new OrderDto { Id = o.OrderID, Total = o.TotalAmount })
// Sorting
orders.OrderBy(o => o.OrderDate)
orders.OrderByDescending(o => o.TotalAmount)
orders.OrderBy(o => o.CustomerID).ThenBy(o => o.OrderDate)
// Aggregation
orders.Count()
orders.Count(o => o.Status == "Active")
orders.Sum(o => o.TotalAmount)
orders.Average(o => o.TotalAmount)
orders.Max(o => o.TotalAmount)
// First / Single
orders.First() // throws if empty
orders.FirstOrDefault() // null if empty
orders.Single() // throws if not exactly 1 result
orders.SingleOrDefault() // null if empty, throws if more than 1
// Existence checks
orders.Any() // true if any rows
orders.Any(o => o.Total > 0)
orders.All(o => o.Status != "Cancelled")
// Pagination
orders.Skip(20).Take(10) // Page 3 of 10 per page
// Joining
var result = orders.Join(
customers,
o => o.CustomerID,
c => c.CustomerID,
(o, c) => new { o.OrderID, c.Name });
// GroupBy
var grouped = orders
.GroupBy(o => o.CustomerID)
.Select(g => new {
CustomerID = g.Key,
Count = g.Count(),
Total = g.Sum(o => o.TotalAmount)
});
// Flattening
var items = orders.SelectMany(o => o.OrderItems);
// Set operations
var merged = list1.Union(list2);
var common = list1.Intersect(list2);
var onlyLeft = list1.Except(list2);
// Distinct
orders.Select(o => o.CustomerID).Distinct();
// ToLookup (like Dictionary<TKey, IEnumerable<TValue>>)
var byCustomer = orders.ToLookup(o => o.CustomerID);
var customerOrders = byCustomer[customerId]; // O(1) lookup
LINQ Performance Tips
- Call
.Where()and filtering operators before.Select()to reduce the working set early - Use
.Any()instead of.Count() > 0(stops at first match) - Use
.FirstOrDefault()instead of.Where(...).First()(same result, slightly cleaner) - Materialize with
.ToList()or.ToArray()when you need to iterate multiple times - For EF Core: avoid
.ToList()before filtering โ let the DB do the work