Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

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!

📅 Timeline - LINQ w C#
  • C# 3.0 (2007) - 🔥 LINQ introduced! Query syntax, method syntax
  • C# 3.0 (2007) - Extension methods, lambda expressions, anonymous types
  • .NET 3.5 (2007) - LINQ to Objects, LINQ to SQL, LINQ to XML
  • EF 4+ (2010+) - LINQ to Entities (Entity Framework)
  • C# 6+ (2015+) - Improved LINQ performance, optimizations
  • .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 - fluent API

🎉 C# 3.0 - LINQ Method Syntax

Extension methods + lambda expressions! .Where().Select()

// 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ą ✨
⚠️ Deferred Execution - pułapki!
  • ❌ Multiple enumeration = multiple executions (performance!)
  • ❌ Source collection modified = query results change
  • ❌ Closures captured - zmienne mogą się zmienić
// 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();
Podsumowanie

LINQ - rewolucja w pracy z kolekcjami!

  • Problem przed LINQ - verbose loops, boilerplate
  • Query syntax - SQL-like, dobry dla joins
  • Method syntax - fluent API, standard w 2026! ✨
  • Where, Select, SelectMany - filtering, projection, flattening
  • GroupBy, OrderBy - grouping, sorting
  • 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();
🎯 BONUS: Advanced LINQ Query Builder

Stwórz advanced query builder używając LINQ + dynamic queries!

Do zaimplementowania:

  1. QueryBuilder class:
    • Fluent API dla budowania queries
    • Dynamic filtering (runtime conditions)
    • Dynamic sorting
    • Pagination support
  2. Features:
    • Where(condition, predicate) - conditional filters
    • OrderBy(field, direction) - dynamic sorting
    • Page(number, size) - pagination
    • Select(fields) - projection
  3. Support both:
    • LINQ to Objects (IEnumerable)
    • LINQ to EF (IQueryable)
  4. Statistics tracking:
    • Total count (przed filtering)
    • Filtered count (po filtering)
    • Page count
    • Execution time

Przykład użycia:

// Scenario: API endpoint z dynamic filtering/sorting
public async Task<PagedResult<PersonDto>> GetPeople(
    string? city,
    int? minAge,
    int? maxAge,
    string? sortBy,
    string? sortDirection,
    int page,
    int pageSize)
{
    var query = new QueryBuilder<Person>(_dbContext.People)
        .WhereIf(city != null, p => p.City == city)
        .WhereIf(minAge != null, p => p.Age >= minAge)
        .WhereIf(maxAge != null, p => p.Age <= maxAge)
        .OrderBy(sortBy ?? "Name", sortDirection ?? "asc")
        .Page(page, pageSize)
        .Select(p => new PersonDto 
        { 
            Name = p.Name, 
            Age = p.Age, 
            City = p.City 
        });
    
    var result = await query.ExecuteAsync();
    
    return new PagedResult<PersonDto>
    {
        Items = result.Items,
        TotalCount = result.TotalCount,
        PageNumber = page,
        PageSize = pageSize,
        TotalPages = result.PageCount
    };
}

// Wywołanie:
// GET /api/people?city=Warsaw&minAge=18&sortBy=Age&sortDirection=desc&page=2&pageSize=10

// Generated SQL:
// SELECT [Name], [Age], [City]
// FROM [People]
// WHERE [City] = 'Warsaw' AND [Age] >= 18
// ORDER BY [Age] DESC
// OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY

Ten projekt demonstruje advanced LINQ - dynamic queries, fluent API, LINQ to EF, pagination, wszystko razem! 🚀🔍