LINQ to jedna z najważniejszych rewolucji w historii C#! Wprowadzony w C# 3.0 (2007), LINQ zmienił sposób w jaki pracujemy z kolekcjami. W 2015 roku LINQ był już dojrzały. W 2026 roku to fundamenty - każdy developer używa LINQ codziennie!
W tym wpisie poznasz query syntax vs method syntax, najważniejsze operatory (Where, Select, GroupBy), join operations, deferred execution, różnice między LINQ to Objects i LINQ to SQL/EF, i jak tworzyć własne LINQ operatory!
.NET 6+ (2021+) - New LINQ methods: Chunk, DistinctBy, MaxBy
Problem przed LINQ - verbose loops
Filtering bez LINQ - boilerplate
// Przed LINQ (C# 1.0-2.0) - ręczne loops i if statements
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Znajdź liczby parzyste
List<int> evenNumbers = new List<int>();
foreach (int num in numbers)
{
if (num % 2 == 0)
{
evenNumbers.Add(num);
}
}
// [2, 4, 6, 8, 10]
// Znajdź liczby > 5
List<int> greaterThanFive = new List<int>();
foreach (int num in numbers)
{
if (num > 5)
{
greaterThanFive.Add(num);
}
}
// [6, 7, 8, 9, 10]
// Transform do string
List<string> strings = new List<string>();
foreach (int num in numbers)
{
strings.Add(num.ToString());
}
// ["1", "2", "3", ...]
// Verbose! Boilerplate! 😱
Grouping i aggregation - jeszcze gorzej
// Groupowanie bez LINQ - HORROR
List<Person> people = GetPeople();
// Group by City
Dictionary<string, List<Person>> groupedByCity = new Dictionary<string, List<Person>>();
foreach (var person in people)
{
if (!groupedByCity.ContainsKey(person.City))
{
groupedByCity[person.City] = new List<Person>();
}
groupedByCity[person.City].Add(person);
}
// Calculate average age per city
Dictionary<string, double> avgAgeByCity = new Dictionary<string, double>();
foreach (var group in groupedByCity)
{
int sum = 0;
int count = 0;
foreach (var person in group.Value)
{
sum += person.Age;
count++;
}
avgAgeByCity[group.Key] = (double)sum / count;
}
// 25+ linii kodu! 😱
LINQ - rewolucja! Query Syntax vs Method Syntax
LINQ Query Syntax - SQL-like
🎉 C# 3.0 - LINQ Query Syntax
SQL-like syntax w C#! from ... where ... select
// LINQ Query Syntax - SQL-like
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Znajdź liczby parzyste
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
// [2, 4, 6, 8, 10]
// Znajdź liczby > 5
var greaterThanFive = from num in numbers
where num > 5
select num;
// [6, 7, 8, 9, 10]
// Transform do string
var strings = from num in numbers
select num.ToString();
// ["1", "2", "3", ...]
// Zwięźle! Czytelnie! ✨
// LINQ Method Syntax - extension methods + lambdas
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Znajdź liczby parzyste
var evenNumbers = numbers.Where(num => num % 2 == 0);
// [2, 4, 6, 8, 10]
// Znajdź liczby > 5
var greaterThanFive = numbers.Where(num => num > 5);
// [6, 7, 8, 9, 10]
// Transform do string
var strings = numbers.Select(num => num.ToString());
// ["1", "2", "3", ...]
// Łańcuchowanie (method chaining)
var result = numbers
.Where(num => num % 2 == 0) // Parzyste
.Where(num => num > 5) // > 5
.Select(num => num * 2); // * 2
// [12, 14, 16, 18, 20]
// Zwięźle! Eleganckie! ✨
Query Syntax
// Query syntax - SQL-like
var result = from num in numbers
where num % 2 == 0
where num > 5
select num * 2;
// Zalety:
// ✅ Znajomy dla SQL developers
// ✅ Czytelny dla złożonych queries
// ✅ Join syntax jest czytelniejszy
// Wady:
// ❌ Nie wszystkie operatory (np. Take, Skip)
// ❌ Trudniejsze łańcuchowanie
// ❌ Verbose dla prostych operacji
Method Syntax (ZALECANE)
// Method syntax - fluent API
var result = numbers
.Where(num => num % 2 == 0)
.Where(num => num > 5)
.Select(num => num * 2);
// Zalety:
// ✅ WSZYSTKIE operatory dostępne
// ✅ Łatwe łańcuchowanie
// ✅ Krótsze dla prostych operacji
// ✅ Bardziej "C#-like"
// To jest standard w 2026! ✨
🔍 Query vs Method Syntax - co wybrać?
Method syntax to standard w 2026 roku - 95%+ developers używa method syntax. Query syntax używaj tylko dla złożonych joins/group by gdzie jest czytelniejszy.
W tym kursie skupimy się na method syntax jako primary approach!
Najważniejsze operatory LINQ
Where - filtering
// Where - filtrowanie elementów
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Single condition
var evenNumbers = numbers.Where(n => n % 2 == 0);
// [2, 4, 6, 8, 10]
// Multiple conditions (and)
var result = numbers.Where(n => n % 2 == 0 && n > 5);
// [6, 8, 10]
// Łańcuchowanie Where (równoważne)
var result2 = numbers
.Where(n => n % 2 == 0)
.Where(n => n > 5);
// [6, 8, 10]
// Z complex objects
List<Person> people = GetPeople();
var adults = people.Where(p => p.Age >= 18);
var warsawAdults = people.Where(p => p.Age >= 18 && p.City == "Warsaw");
Select - projection/transformation
// Select - transformacja elementów
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Simple transformation
var doubled = numbers.Select(n => n * 2);
// [2, 4, 6, 8, 10]
var strings = numbers.Select(n => n.ToString());
// ["1", "2", "3", "4", "5"]
// Z complex objects - wybierz tylko jedno pole
List<Person> people = GetPeople();
var names = people.Select(p => p.Name);
// ["Jan", "Anna", "Piotr", ...]
var ages = people.Select(p => p.Age);
// [30, 25, 35, ...]
// Projection do nowego typu (anonymous type)
var personInfo = people.Select(p => new
{
p.Name,
p.Age,
IsAdult = p.Age >= 18
});
// [{ Name = "Jan", Age = 30, IsAdult = true }, ...]
// Projection z index
var numberedNames = people.Select((p, index) => $"{index + 1}. {p.Name}");
// ["1. Jan", "2. Anna", "3. Piotr", ...]
SelectMany - flattening
// SelectMany - flatten nested collections
List<Order> orders = GetOrders();
// Każdy Order ma List<OrderItem>
// Chcemy wszystkie items z wszystkich orders
// ❌ Select zwraca IEnumerable<IEnumerable<OrderItem>> (nested!)
var nestedItems = orders.Select(o => o.Items);
// ✅ SelectMany flatten-uje do IEnumerable<OrderItem>
var allItems = orders.SelectMany(o => o.Items);
// Wszystkie items z wszystkich orders w jednej flat liście!
// Przykład z int arrays
int[][] arrays = new int[][]
{
new int[] { 1, 2, 3 },
new int[] { 4, 5, 6 },
new int[] { 7, 8, 9 }
};
var flattened = arrays.SelectMany(arr => arr);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// SelectMany z projection
var orderDetails = orders.SelectMany(o => o.Items,
(order, item) => new
{
OrderId = order.Id,
ProductName = item.ProductName,
Quantity = item.Quantity
});
GroupBy - grouping
// GroupBy - grupowanie elementów
List<Person> people = GetPeople();
// Group by City
var groupedByCity = people.GroupBy(p => p.City);
// groupedByCity to IEnumerable<IGrouping<string, Person>>
// Każdy group ma Key (city name) i elementy (people)
foreach (var group in groupedByCity)
{
Console.WriteLine($"City: {group.Key}");
foreach (var person in group)
{
Console.WriteLine($" - {person.Name}");
}
}
// Output:
// City: Warsaw
// - Jan
// - Anna
// City: Krakow
// - Piotr
// - Maria
// GroupBy z aggregation
var cityCounts = people
.GroupBy(p => p.City)
.Select(g => new
{
City = g.Key,
Count = g.Count(),
AvgAge = g.Average(p => p.Age)
});
// [{ City = "Warsaw", Count = 2, AvgAge = 27.5 }, ...]
// GroupBy multiple keys
var groupedByAgeAndCity = people.GroupBy(p => new { p.Age, p.City });
foreach (var group in groupedByAgeAndCity)
{
Console.WriteLine($"Age: {group.Key.Age}, City: {group.Key.City}");
Console.WriteLine($"Count: {group.Count()}");
}
OrderBy, ThenBy - sorting
// OrderBy - sortowanie
List<Person> people = GetPeople();
// Sortuj po Age (ascending)
var byAge = people.OrderBy(p => p.Age);
// Sortuj po Age (descending)
var byAgeDesc = people.OrderByDescending(p => p.Age);
// Multiple sort keys - ThenBy
var sorted = people
.OrderBy(p => p.City) // Najpierw po City
.ThenBy(p => p.Age) // Potem po Age
.ThenByDescending(p => p.Name); // Potem po Name (desc)
// Numbers
List<int> numbers = new List<int> { 5, 2, 8, 1, 9, 3 };
var ascending = numbers.OrderBy(n => n);
// [1, 2, 3, 5, 8, 9]
var descending = numbers.OrderByDescending(n => n);
// [9, 8, 5, 3, 2, 1]
Inne ważne operatory
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Take - weź pierwsze N
var firstThree = numbers.Take(3);
// [1, 2, 3]
// Skip - pomiń pierwsze N
var skipTwo = numbers.Skip(2);
// [3, 4, 5, 6, 7, 8, 9, 10]
// Take + Skip = pagination
int pageSize = 3;
int pageNumber = 2;
var page = numbers.Skip((pageNumber - 1) * pageSize).Take(pageSize);
// [4, 5, 6]
// First - pierwszy element (exception jeśli brak)
var first = numbers.First(); // 1
var firstEven = numbers.First(n => n % 2 == 0); // 2
// FirstOrDefault - pierwszy lub default (null/0)
var firstOrDefault = numbers.FirstOrDefault(n => n > 100); // 0 (brak)
// Single - dokładnie jeden element (exception jeśli 0 lub >1)
var single = numbers.Where(n => n == 5).Single(); // 5
// Any - czy istnieje jakikolwiek element spełniający warunek
bool hasEven = numbers.Any(n => n % 2 == 0); // true
bool hasNegative = numbers.Any(n => n < 0); // false
// All - czy wszystkie elementy spełniają warunek
bool allPositive = numbers.All(n => n > 0); // true
bool allEven = numbers.All(n => n % 2 == 0); // false
// Count - liczba elementów
int count = numbers.Count(); // 10
int evenCount = numbers.Count(n => n % 2 == 0); // 5
// Sum, Average, Min, Max
int sum = numbers.Sum(); // 55
double avg = numbers.Average(); // 5.5
int min = numbers.Min(); // 1
int max = numbers.Max(); // 10
// Distinct - unique values
List<int> duplicates = new List<int> { 1, 2, 2, 3, 3, 3, 4 };
var unique = duplicates.Distinct();
// [1, 2, 3, 4]
// .NET 6+ - DistinctBy
List<Person> people = GetPeople();
var uniqueByCity = people.DistinctBy(p => p.City);
// .NET 6+ - Chunk
var chunks = numbers.Chunk(3);
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
Join Operations - łączenie kolekcji
Join - inner join
// Inner Join - łączenie kolekcji po kluczu
List<Customer> customers = GetCustomers();
List<Order> orders = GetOrders();
// Join customers z orders po CustomerId
var customerOrders = customers.Join(
orders, // Inner collection
customer => customer.Id, // Outer key selector
order => order.CustomerId, // Inner key selector
(customer, order) => new // Result selector
{
CustomerName = customer.Name,
OrderId = order.Id,
OrderTotal = order.Total
}
);
// Tylko customers którzy mają orders (inner join)
// Query syntax (czasami czytelniejszy dla joins)
var customerOrders2 =
from customer in customers
join order in orders on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
OrderId = order.Id,
OrderTotal = order.Total
};
GroupJoin - left outer join
// GroupJoin - każdy customer z kolekcją jego orders
var customersWithOrders = customers.GroupJoin(
orders,
customer => customer.Id,
order => order.CustomerId,
(customer, customerOrders) => new
{
Customer = customer,
Orders = customerOrders.ToList()
}
);
// WSZYSCY customers, nawet ci bez orders (left join)
// Query syntax
var customersWithOrders2 =
from customer in customers
join order in orders on customer.Id equals order.CustomerId into customerOrders
select new
{
Customer = customer,
Orders = customerOrders.ToList()
};
// Left Outer Join - customers z lub bez orders
var leftJoin =
from customer in customers
join order in orders on customer.Id equals order.CustomerId into customerOrders
from order in customerOrders.DefaultIfEmpty()
select new
{
CustomerName = customer.Name,
OrderId = order?.Id ?? 0,
OrderTotal = order?.Total ?? 0
};
Praktyczne przykłady joins
// Przykład: Customer-Order-OrderItem hierarchy
List<Customer> customers = GetCustomers();
List<Order> orders = GetOrders();
List<OrderItem> orderItems = GetOrderItems();
// Multi-level join
var fullOrderDetails =
from customer in customers
join order in orders on customer.Id equals order.CustomerId
join item in orderItems on order.Id equals item.OrderId
select new
{
CustomerName = customer.Name,
OrderId = order.Id,
ProductName = item.ProductName,
Quantity = item.Quantity,
Price = item.Price
};
// Method syntax
var fullOrderDetails2 = customers
.Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c, o })
.Join(orderItems, x => x.o.Id, i => i.OrderId, (x, i) => new
{
CustomerName = x.c.Name,
OrderId = x.o.Id,
ProductName = i.ProductName,
Quantity = i.Quantity,
Price = i.Price
});
// Total sales per customer
var customerSales = customers
.GroupJoin(orders, c => c.Id, o => o.CustomerId, (c, orders) => new
{
CustomerName = c.Name,
TotalSales = orders.Sum(o => o.Total),
OrderCount = orders.Count()
});
Deferred vs Immediate Execution - KLUCZOWE!
Deferred Execution - query nie wykonuje się od razu
// DEFERRED EXECUTION - query jest tylko definicją!
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// To NIE wykonuje query! Tylko definiuje!
var query = numbers.Where(n => n % 2 == 0);
Console.WriteLine("Query defined");
// Query wykonuje się DOPIERO gdy iterated!
foreach (var num in query)
{
Console.WriteLine(num); // TERAZ query się wykonuje!
}
// Modyfikacja source collection WPŁYWA na query!
numbers.Add(6);
numbers.Add(7);
numbers.Add(8);
// Query będzie teraz zawierało 6 i 8!
foreach (var num in query)
{
Console.WriteLine(num); // 2, 4, 6, 8 - query re-executed!
}
Immediate Execution - force execution
// IMMEDIATE EXECUTION - wymusza wykonanie query
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// ToList() - wykonuje query NATYCHMIAST i tworzy kopię
var list = numbers.Where(n => n % 2 == 0).ToList();
// ToArray() - wykonuje query NATYCHMIAST i tworzy array
var array = numbers.Where(n => n % 2 == 0).ToArray();
// ToDictionary() - wykonuje query NATYCHMIAST
var dict = numbers.ToDictionary(n => n, n => n * 2);
// Count(), Sum(), Average(), etc. - wykonują query NATYCHMIAST
int count = numbers.Where(n => n % 2 == 0).Count(); // Executes NOW
int sum = numbers.Where(n => n % 2 == 0).Sum(); // Executes NOW
// First(), Single(), etc. - wykonują query NATYCHMIAST
int first = numbers.Where(n => n % 2 == 0).First(); // Executes NOW
// Modyfikacja source collection NIE wpływa!
numbers.Add(6);
numbers.Add(8);
Console.WriteLine(list.Count); // 2 - unchanged! (snapshot)
Deferred (Query, Where, Select)
var query = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 2);
// Query NIE wykonane!
// Każde foreach = re-execution
foreach (var n in query) { } // Execute
foreach (var n in query) { } // Execute again!
// Modyfikacje numbers wpływają na query
Immediate (ToList, Count, Sum)
var list = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 2)
.ToList(); // Execute NOW!
// Query już wykonane!
// Foreach = iteracja po liście
foreach (var n in list) { } // No execution
foreach (var n in list) { } // No execution
// Modyfikacje numbers NIE wpływają ✨
// Pułapka 1: Multiple enumeration
var query = numbers.Where(n => ExpensiveCheck(n));
int count = query.Count(); // Execute 1x
var list = query.ToList(); // Execute 2x - BAD!
// Fix: Execute once
var list = query.ToList();
int count = list.Count; // No execution!
// Pułapka 2: Closure capture
var queries = new List<IEnumerable<int>>();
for (int i = 0; i < 5; i++)
{
queries.Add(numbers.Where(n => n < i)); // Captures i!
}
// i == 5 teraz!
foreach (var q in queries)
{
Console.WriteLine(q.Count()); // Wszystkie 5! (i==5 dla wszystkich)
}
LINQ to Objects vs LINQ to SQL/EF
LINQ to Objects - in-memory collections
// LINQ to Objects - operacje na in-memory collections
List<Person> people = GetPeopleFromMemory(); // List w pamięci
// Query wykonuje się W PAMIĘCI
var adults = people
.Where(p => p.Age >= 18) // Filtering w C#
.OrderBy(p => p.Name) // Sorting w C#
.Select(p => p.Name); // Projection w C#
// Wszystko dzieje się w aplikacji - C# code
// Możesz użyć dowolnych C# expressions
var result = people
.Where(p => ComplexValidation(p)) // Custom C# method - OK!
.Select(p => TransformPerson(p)); // Custom C# method - OK!
LINQ to SQL/EF - database queries
// LINQ to EF - operacje na database
using var db = new MyDbContext();
// Query jest translated do SQL!
var adults = db.People
.Where(p => p.Age >= 18) // → WHERE Age >= 18
.OrderBy(p => p.Name) // → ORDER BY Name
.Select(p => p.Name); // → SELECT Name
// Generated SQL:
// SELECT [Name] FROM [People]
// WHERE [Age] >= 18
// ORDER BY [Name]
// Query wykonuje się NA SERWERZE (database)!
// ❌ Nie możesz użyć custom C# methods!
var result = db.People
.Where(p => ComplexValidation(p)) // ❌ BŁĄD - nie można translate do SQL!
.ToList();
LINQ to EF - deferred execution i IQueryable
// LINQ to EF używa IQueryable<T> (nie IEnumerable<T>)
using var db = new MyDbContext();
// To jest IQueryable - query NIE wykonane!
IQueryable<Person> query = db.People
.Where(p => p.Age >= 18)
.Where(p => p.City == "Warsaw");
// Możesz dalej budować query
query = query.OrderBy(p => p.Name);
// Query wykonuje się DOPIERO gdy:
// 1. ToList() / ToArray()
var list = query.ToList(); // TERAZ query idzie do DB!
// 2. Count() / Sum() / etc.
int count = query.Count(); // SELECT COUNT(*)
// 3. First() / Single() / etc.
var first = query.First(); // SELECT TOP 1
// 4. foreach
foreach (var person in query) // SELECT * FROM People WHERE...
{
Console.WriteLine(person.Name);
}
// Zaleta: można budować query dynamicznie!
IQueryable<Person> BuildQuery(MyDbContext db, string? city, int? minAge)
{
var query = db.People.AsQueryable();
if (city != null)
query = query.Where(p => p.City == city);
if (minAge != null)
query = query.Where(p => p.Age >= minAge);
return query;
}
// Final SQL będzie zawierał tylko używane WHERE clauses!
LINQ to EF - client vs server evaluation
using var db = new MyDbContext();
// ✅ Server evaluation - wszystko w SQL
var serverQuery = db.People
.Where(p => p.Age >= 18) // SQL: WHERE Age >= 18
.Select(p => p.Name.ToUpper()); // SQL: UPPER(Name)
.ToList();
// ❌ Client evaluation - częściowo w C# (WARNING w EF Core 3.0+)
var clientQuery = db.People
.Where(p => p.Age >= 18) // SQL: WHERE Age >= 18
.ToList() // TERAZ fetch z DB
.Where(p => ComplexValidation(p)) // C#: po fetch
.ToList();
// Problem: fetch WSZYSTKIE Age >= 18, potem filter w C#
// Może fetch 10,000 rows zamiast 10!
// ✅ Fix: AsEnumerable() explicit
var explicitQuery = db.People
.Where(p => p.Age >= 18) // SQL
.AsEnumerable() // Switch do LINQ to Objects
.Where(p => ComplexValidation(p)) // C# (explicit)
.ToList();
Feature
LINQ to Objects
LINQ to EF
Source
In-memory (List, Array)
Database (SQL Server, etc.)
Type
IEnumerable<T>
IQueryable<T>
Execution
In C# process
On database server
Custom methods
✅ Dowolne C# code
❌ Tylko translatable do SQL
Performance
Limit: pamięć
Może handle miliony rows
Deferred execution
✅ Tak
✅ Tak
Custom LINQ Operators - extension methods
LINQ operators to extension methods!
// LINQ operators to extension methods na IEnumerable<T>!
// Where jest extension method:
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
{
foreach (var item in source)
{
if (predicate(item))
yield return item;
}
}
// Select jest extension method:
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach (var item in source)
{
yield return selector(item);
}
}
// To znaczy możesz tworzyć WŁASNE LINQ operators! ✨
Custom operator - WhereNot
// Custom LINQ operator - WhereNot (negation)
public static class MyLinqExtensions
{
public static IEnumerable<T> WhereNot<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
{
foreach (var item in source)
{
if (!predicate(item)) // Negation!
yield return item;
}
}
}
// Użycie
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var notEven = numbers.WhereNot(n => n % 2 == 0);
// [1, 3, 5] - odd numbers
// Zamiast:
var notEven2 = numbers.Where(n => !(n % 2 == 0)); // Verbose
Custom operator - Batch/Chunk
// Custom operator - Batch (pre .NET 6)
public static class MyLinqExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source,
int batchSize)
{
var batch = new List<T>(batchSize);
foreach (var item in source)
{
batch.Add(item);
if (batch.Count == batchSize)
{
yield return batch;
batch = new List<T>(batchSize);
}
}
if (batch.Count > 0)
yield return batch;
}
}
// Użycie
List<int> numbers = Enumerable.Range(1, 10).ToList();
var batches = numbers.Batch(3);
foreach (var batch in batches)
{
Console.WriteLine(string.Join(", ", batch));
}
// Output:
// 1, 2, 3
// 4, 5, 6
// 7, 8, 9
// 10
// .NET 6+ ma built-in Chunk()
var chunks = numbers.Chunk(3);
Custom operator - ForEach
// Custom operator - ForEach (side effects)
public static class MyLinqExtensions
{
public static void ForEach<T>(
this IEnumerable<T> source,
Action<T> action)
{
foreach (var item in source)
{
action(item);
}
}
}
// Użycie
List<string> names = new List<string> { "Jan", "Anna", "Piotr" };
names.ForEach(name => Console.WriteLine(name));
// Zamiast:
foreach (var name in names)
{
Console.WriteLine(name);
}
Custom operator - Praktyczne przykłady
public static class AdvancedLinqExtensions
{
// DistinctBy (przed .NET 6)
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
var seenKeys = new HashSet<TKey>();
foreach (var element in source)
{
if (seenKeys.Add(keySelector(element)))
yield return element;
}
}
// MaxBy (przed .NET 6)
public static TSource? MaxBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector) where TKey : IComparable<TKey>
{
TSource? max = default;
TKey? maxKey = default;
bool first = true;
foreach (var item in source)
{
var key = keySelector(item);
if (first || key.CompareTo(maxKey) > 0)
{
max = item;
maxKey = key;
first = false;
}
}
return max;
}
// Tap - peek without consuming
public static IEnumerable<T> Tap<T>(
this IEnumerable<T> source,
Action<T> action)
{
foreach (var item in source)
{
action(item);
yield return item;
}
}
}
// Użycie
List<Person> people = GetPeople();
var oldest = people.MaxBy(p => p.Age);
var uniqueByCity = people.DistinctBy(p => p.City);
var result = numbers
.Where(n => n % 2 == 0)
.Tap(n => Console.WriteLine($"Debug: {n}")) // Logging bez przerywania chain
.Select(n => n * 2)
.ToList();
✅ Join operations - Join, GroupJoin, left outer join
✅ Deferred vs Immediate execution - KLUCZOWE zrozumienie!
✅ LINQ to Objects vs EF - in-memory vs database
✅ IEnumerable vs IQueryable - różnice, client vs server
✅ Custom LINQ operators - extension methods, własne operatory
W kolejnym wpisie poznasz delegaty, eventy i wyrażenia lambda!
Zadanie dla Ciebie 🎯
Stwórz zestaw custom LINQ operators:
public static class CustomLinqExtensions
{
// 1. WhereIf - conditional filtering
public static IEnumerable<T> WhereIf<T>(
this IEnumerable<T> source,
bool condition,
Func<T, bool> predicate);
// 2. Paginate - pagination helper
public static IEnumerable<T> Paginate<T>(
this IEnumerable<T> source,
int pageNumber,
int pageSize);
// 3. DistinctBy - distinct by key selector
public static IEnumerable<T> DistinctBy<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> keySelector);
// 4. Tap - side effect without consuming
public static IEnumerable<T> Tap<T>(
this IEnumerable<T> source,
Action<T> action);
}
// Przykład użycia:
var result = people
.WhereIf(includeInactive, p => !p.IsActive)
.DistinctBy(p => p.Email)
.Tap(p => Console.WriteLine($"Processing: {p.Name}"))
.Paginate(pageNumber: 2, pageSize: 10)
.ToList();