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

Delegaty i lambdy to fundamenty programowania funkcyjnego w C#! W 2015 roku używałeś delegatów i prostych lambd. W 2026 roku masz ulepszone lambdy (C# 10-14), naturalne typy, lambdy z atrybutami, i drzewa wyrażeń!

W tym wpisie poznasz podstawy delegatów, Action/Func/Predicate, events i event handling, wszystkie formy lambda expressions, nowoczesne feature'y lambd, oraz zrobimy wprowadzenie do expression trees!

📅 Timeline - ewolucja delegates i lambd
  • C# 1.0 (2002) - Delegates, multicast delegates
  • C# 2.0 (2005) - Anonymous methods, covariance/contravariance
  • C# 3.0 (2007) - 🔥 Lambda expressions! Expression trees
  • C# 3.0 (2007) - Func<T>, Action<T>, Predicate<T>
  • C# 9.0 (2020) - Static lambdas, discard parameters
  • C# 10 (2021) - 🔥 Natural type dla lambd, explicit return types
  • C# 14 (2026) - 🔥 Lambdy z ref/out/in/params parameters!
Delegates - podstawy

Czym jest delegate?

🔍 Delegate to type-safe function pointer

Delegate = typ który reprezentuje referencję do metody z konkretną sygnaturą
Możesz przechowywać metody w zmiennych, przekazywać jako parametry, wywoływać dynamicznie

// Definicja delegate - type który reprezentuje metodę
public delegate int MathOperation(int a, int b);

// Metody które pasują do delegate (ta sama sygnatura)
public int Add(int a, int b)
{
    return a + b;
}

public int Multiply(int a, int b)
{
    return a * b;
}

// Używanie delegate
MathOperation operation;

operation = Add;  // Przypisz metodę do delegate
int result1 = operation(5, 3);  // Wywołaj przez delegate - 8

operation = Multiply;  // Zmień na inną metodę
int result2 = operation(5, 3);  // 15

// Delegate przechowuje referencję do metody!

Delegates jako parametry - callback pattern

// Delegate jako parametr - callback pattern
public delegate void ProgressCallback(int percentComplete);

public void DownloadFile(string url, ProgressCallback onProgress)
{
    for (int i = 0; i <= 100; i += 10)
    {
        // Simulate download
        Thread.Sleep(100);
        
        // Call callback
        onProgress(i);  // Callback do callera!
    }
}

// Użycie - przekaż metodę jako callback
void ReportProgress(int percent)
{
    Console.WriteLine($"Downloaded: {percent}%");
}

DownloadFile("http://example.com/file.zip", ReportProgress);

// Output:
// Downloaded: 0%
// Downloaded: 10%
// Downloaded: 20%
// ...
// Downloaded: 100%

Multicast delegates - łańcuchowanie

// Multicast delegates - można dodawać wiele metod!
public delegate void Notify(string message);

void SendEmail(string message)
{
    Console.WriteLine($"Email: {message}");
}

void SendSMS(string message)
{
    Console.WriteLine($"SMS: {message}");
}

void LogToFile(string message)
{
    Console.WriteLine($"Log: {message}");
}

// Multicast - combine multiple methods
Notify notifier = SendEmail;
notifier += SendSMS;      // Add
notifier += LogToFile;    // Add

// Wywołanie - WSZYSTKIE metody są wywołane!
notifier("System alert!");

// Output:
// Email: System alert!
// SMS: System alert!
// Log: System alert!

// Remove method
notifier -= SendSMS;  // Remove SMS

notifier("Another alert");
// Output:
// Email: Another alert
// Log: Another alert
Action, Func, Predicate - built-in delegates

Action<T> - void methods

🎉 C# 3.0 - Action<T>

Action<T> - delegate dla metod które NIE zwracają wartości (void)

// Action - void method bez parametrów
Action greet = () => Console.WriteLine("Hello!");
greet();  // "Hello!"

// Action<T> - void method z 1 parametrem
Action<string> greetPerson = name => Console.WriteLine($"Hello, {name}!");
greetPerson("Jan");  // "Hello, Jan!"

// Action<T1, T2> - void method z 2 parametrami
Action<string, int> displayInfo = (name, age) => 
    Console.WriteLine($"{name} is {age} years old");
displayInfo("Anna", 25);  // "Anna is 25 years old"

// Action<T1, T2, ..., T16> - do 16 parametrów!
Action<int, int, int> printSum = (a, b, c) => 
    Console.WriteLine($"Sum: {a + b + c}");
printSum(1, 2, 3);  // "Sum: 6"

// Użycie jako parametr
void ProcessItems(List<string> items, Action<string> processor)
{
    foreach (var item in items)
    {
        processor(item);
    }
}

var names = new List<string> { "Jan", "Anna", "Piotr" };
ProcessItems(names, name => Console.WriteLine(name.ToUpper()));
// JAN
// ANNA
// PIOTR

Func<T, TResult> - methods z return value

🎉 C# 3.0 - Func<T, TResult>

Func<T, TResult> - delegate dla metod które ZWRACAJĄ wartość

// Func<TResult> - method bez parametrów, zwraca TResult
Func<int> getRandomNumber = () => Random.Shared.Next(1, 100);
int number = getRandomNumber();

// Func<T, TResult> - method z 1 parametrem, zwraca TResult
Func<int, int> square = x => x * x;
int result = square(5);  // 25

// Func<T1, T2, TResult> - method z 2 parametrami, zwraca TResult
Func<int, int, int> add = (a, b) => a + b;
int sum = add(3, 4);  // 7

// Func<T1, ..., T16, TResult> - do 16 parametrów + return!
Func<string, int, bool> isLongerThan = (text, length) => text.Length > length;
bool isLong = isLongerThan("Hello", 3);  // true

// LINQ używa Func<T, bool> dla Where!
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);  // Func<int, bool>

// LINQ używa Func<T, TResult> dla Select!
var doubled = numbers.Select(n => n * 2);  // Func<int, int>

// Użycie jako parametr - transform pattern
TResult Transform<T, TResult>(T input, Func<T, TResult> transformer)
{
    return transformer(input);
}

string text = "hello";
string upper = Transform(text, s => s.ToUpper());  // "HELLO"
int length = Transform(text, s => s.Length);  // 5

Predicate<T> - boolean check

// Predicate<T> - method która zwraca bool
// Predicate<T> == Func<T, bool>

Predicate<int> isEven = x => x % 2 == 0;
bool result = isEven(4);  // true

Predicate<string> isNullOrEmpty = s => string.IsNullOrEmpty(s);
bool empty = isNullOrEmpty("");  // true

// List<T>.FindAll używa Predicate<T>
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
// [2, 4, 6]

// W nowoczesnym C# używa się Func<T, bool> zamiast Predicate<T>
// Predicate jest legacy, ale nadal działa
Delegate Type Sygnatura Przykład
Action void Method() Action greet = () => Console.WriteLine("Hi");
Action<T> void Method(T) Action<string> print = s => Console.WriteLine(s);
Action<T1, T2> void Method(T1, T2) Action<int, int> add = (a, b) => Console.WriteLine(a+b);
Func<TResult> TResult Method() Func<int> getRandom = () => Random.Shared.Next();
Func<T, TResult> TResult Method(T) Func<int, int> square = x => x * x;
Func<T1, T2, TResult> TResult Method(T1, T2) Func<int, int, int> add = (a, b) => a + b;
Predicate<T> bool Method(T) Predicate<int> isEven = x => x % 2 == 0;
Events i Event Handling

Events - publisher/subscriber pattern

// Event = specjalny delegate dla publisher/subscriber pattern
public class Button
{
    // Delegate dla event
    public delegate void ClickHandler(object sender, EventArgs e);
    
    // Event - używa delegate
    public event ClickHandler Click;
    
    public void PerformClick()
    {
        // Wywołaj event (jeśli ktoś subskrybuje)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Subscriber
void OnButtonClick(object sender, EventArgs e)
{
    Console.WriteLine("Button was clicked!");
}

// Użycie
var button = new Button();
button.Click += OnButtonClick;  // Subscribe

button.PerformClick();  // "Button was clicked!"

button.Click -= OnButtonClick;  // Unsubscribe

EventHandler i EventHandler<T> - standard pattern

// .NET ma built-in EventHandler i EventHandler<T>
public class Button
{
    // EventHandler - standard delegate (object sender, EventArgs e)
    public event EventHandler Click;
    
    public void PerformClick()
    {
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// EventHandler<T> - z custom event args
public class DownloadProgressEventArgs : EventArgs
{
    public int PercentComplete { get; set; }
    public long BytesDownloaded { get; set; }
}

public class Downloader
{
    // EventHandler<T> gdzie T : EventArgs
    public event EventHandler<DownloadProgressEventArgs> ProgressChanged;
    
    public void Download(string url)
    {
        for (int i = 0; i <= 100; i += 10)
        {
            // Raise event
            ProgressChanged?.Invoke(this, new DownloadProgressEventArgs
            {
                PercentComplete = i,
                BytesDownloaded = i * 1024
            });
            
            Thread.Sleep(100);
        }
    }
}

// Subscriber
var downloader = new Downloader();
downloader.ProgressChanged += (sender, e) =>
{
    Console.WriteLine($"Progress: {e.PercentComplete}% ({e.BytesDownloaded} bytes)");
};

downloader.Download("http://example.com/file.zip");

Praktyczne przykłady - events

// Przykład 1: PropertyChanged event (INotifyPropertyChanged)
public class Person : INotifyPropertyChanged
{
    private string _name;
    
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));  // Raise event
            }
        }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// Subscriber
var person = new Person();
person.PropertyChanged += (sender, e) =>
{
    Console.WriteLine($"Property {e.PropertyName} changed!");
};

person.Name = "Jan";  // "Property Name changed!"

// Przykład 2: Timer events
var timer = new System.Timers.Timer(1000);  // 1 second

timer.Elapsed += (sender, e) =>
{
    Console.WriteLine($"Tick at {e.SignalTime}");
};

timer.Start();
Thread.Sleep(5000);
timer.Stop();

// Przykład 3: Custom event system
public class OrderProcessor
{
    public event EventHandler<OrderEventArgs> OrderPlaced;
    public event EventHandler<OrderEventArgs> OrderProcessed;
    public event EventHandler<OrderEventArgs> OrderShipped;
    
    public void ProcessOrder(Order order)
    {
        OrderPlaced?.Invoke(this, new OrderEventArgs { Order = order });
        
        // Process...
        Thread.Sleep(1000);
        OrderProcessed?.Invoke(this, new OrderEventArgs { Order = order });
        
        // Ship...
        Thread.Sleep(1000);
        OrderShipped?.Invoke(this, new OrderEventArgs { Order = order });
    }
}

public class OrderEventArgs : EventArgs
{
    public Order Order { get; set; }
}

// Multiple subscribers
var processor = new OrderProcessor();

processor.OrderPlaced += (s, e) => Console.WriteLine("Email: Order placed");
processor.OrderPlaced += (s, e) => Console.WriteLine("SMS: Order placed");
processor.OrderPlaced += (s, e) => LogToDatabase(e.Order);

processor.ProcessOrder(new Order { Id = 123 });
Lambda Expressions - wszystkie formy

Lambda podstawy - od anonymous methods

C# 2.0 - Anonymous methods
// Anonymous method (verbose)
Func<int, int> square = delegate(int x)
{
    return x * x;
};

List<int> numbers = new List<int> { 1, 2, 3 };
var even = numbers.Where(delegate(int n)
{
    return n % 2 == 0;
});
C# 3.0+ - Lambda expressions
// Lambda (zwięźle! ✨)
Func<int, int> square = x => x * x;

List<int> numbers = new List<int> { 1, 2, 3 };
var even = numbers.Where(n => n % 2 == 0);

Lambda syntax - wszystkie formy

// Forma 1: Expression lambda - single expression
Func<int, int> square = x => x * x;

// Forma 2: Statement lambda - block z {}
Func<int, int> square2 = x => 
{
    int result = x * x;
    return result;
};

// Forma 3: Bez parametrów - ()
Func<string> getMessage = () => "Hello";
Action logMessage = () => Console.WriteLine("Log");

// Forma 4: Jeden parametr - bez nawiasów
Func<int, int> double1 = x => x * 2;

// Forma 5: Wiele parametrów - z nawiasami
Func<int, int, int> add = (a, b) => a + b;

// Forma 6: Explicit types (opcjonalne)
Func<int, int, int> multiply = (int a, int b) => a * b;

// Forma 7: Discard parameters (C# 9)
Func<int, int, int> returnFirst = (x, _) => x;  // _ = discard

// Forma 8: Async lambda
Func<Task<string>> fetchData = async () =>
{
    await Task.Delay(100);
    return "Data";
};

Lambda closures - capturing variables

// Lambda może "capture" zmienne z outer scope
int factor = 10;

Func<int, int> multiply = x => x * factor;  // Captures 'factor'

Console.WriteLine(multiply(5));  // 50

factor = 20;  // Zmień factor
Console.WriteLine(multiply(5));  // 100 - lambda widzi nową wartość!

// Closure - lambda "zamyka" nad zmiennymi
List<Func<int>> functions = new List<Func<int>>();

for (int i = 0; i < 5; i++)
{
    int captured = i;  // Capture variable
    functions.Add(() => captured);
}

foreach (var func in functions)
{
    Console.WriteLine(func());  // 0, 1, 2, 3, 4
}

// ❌ Pułapka - capturing loop variable
List<Func<int>> badFunctions = new List<Func<int>>();

for (int i = 0; i < 5; i++)
{
    badFunctions.Add(() => i);  // Captures 'i' (reference!)
}

foreach (var func in badFunctions)
{
    Console.WriteLine(func());  // 5, 5, 5, 5, 5 - wszystkie 5!
}
// i == 5 po pętli, wszystkie lambdy widzą tę samą zmienną!
🔥 Lambda Improvements (C# 10-14)

C# 9 - Static lambdas

// C# 9 - static lambda - NIE może capture zmiennych
int factor = 10;

// ❌ Normal lambda - może capture
Func<int, int> multiply1 = x => x * factor;  // OK - captures factor

// ✅ Static lambda - NIE MOŻE capture (compile error jeśli próbujesz)
// Func<int, int> multiply2 = static x => x * factor;  // ❌ BŁĄD!

// Static lambda - tylko dla performance (no allocation dla closure)
Func<int, int> double1 = static x => x * 2;  // ✅ OK - no capture

// Use case: performance-critical code bez closures
var numbers = Enumerable.Range(1, 1000000);
var doubled = numbers.Select(static x => x * 2);  // No closure allocations!

C# 10 - Natural type dla lambd

🎉 C# 10 - Natural type dla lambd

Lambda może być przypisana do var! Kompilator wywnioskuje typ!

// C# 10 - var z lambda! Natural type inference
var square = (int x) => x * x;  // Type: Func<int, int>

var add = (int a, int b) => a + b;  // Type: Func<int, int, int>

var greet = (string name) => Console.WriteLine($"Hello, {name}");
// Type: Action<string>

// Musisz podać typy parametrów dla var!
// var bad = x => x * 2;  // ❌ BŁĄD - nie wie jaki typ x

// Przekazywanie jako argument - też działa!
void ProcessNumber(Func<int, int> processor)
{
    Console.WriteLine(processor(5));
}

ProcessNumber((int x) => x * x);  // Lambda bez explicit Func<...>!

// Array of lambdas z var
var operations = new[]
{
    (int x) => x * 2,
    (int x) => x * x,
    (int x) => x + 10
};
// Type: Func<int, int>[]

C# 10 - Explicit return type

// C# 10 - możesz określić return type w lambda
var square = int (int x) => x * x;  // Return type: int

var divide = double (int a, int b) => (double)a / b;  // Return: double

// Użyteczne gdy return type nie jest oczywisty
var getValue = object (bool condition) => condition ? 42 : "text";
// Return type: object (bez explicit byłoby error - ambiguous)

// Z async
var fetchData = async Task<string> () =>
{
    await Task.Delay(100);
    return "Data";
};

C# 10 - Attributes na lambdach

// C# 10 - attributes na lambdach i ich parametrach
var validateInput = 
    ([NotNull] string input) => input.Length > 0;

// Multiple attributes
var process = 
    [SomeAttribute]
    [AnotherAttribute]
    (string value) => value.ToUpper();

// Return type + attributes
var compute = 
    [Pure]
    int (int x) => x * x;

🔥 C# 14 - ref/out/in/params w lambdach!

🎉 NOWOŚĆ C# 14 - ref/out/in/params w lambdach!

Lambdy mogą używać ref, out, in, params parameters! Poznane w wpisie #7!

// C# 14 - ref parameter w lambda
var swap = (ref int a, ref int b) =>
{
    (a, b) = (b, a);
};

int x = 5, y = 10;
swap(ref x, ref y);
Console.WriteLine($"{x}, {y}");  // 10, 5

// C# 14 - out parameter w lambda
var tryParse = (string input, out int result) =>
{
    return int.TryParse(input, out result);
};

if (tryParse("123", out int number))
{
    Console.WriteLine(number);  // 123
}

// C# 14 - in parameter w lambda (readonly ref)
var computeSquare = (in int value) => value * value;

int num = 5;
int squared = computeSquare(in num);

// C# 14 - params parameter w lambda
var sum = (params int[] numbers) =>
{
    int total = 0;
    foreach (int n in numbers)
        total += n;
    return total;
};

int result = sum(1, 2, 3, 4, 5);  // 15
Expression Trees - wprowadzenie

Expression<T> - lambda jako data

// Normal lambda - compiled do kodu
Func<int, int> squareFunc = x => x * x;
int result = squareFunc(5);  // 25 - wykonuje kod

// Expression tree - lambda jako DATA (structure)
Expression<Func<int, int>> squareExpr = x => x * x;

// squareExpr to drzewo reprezentujące wyrażenie!
// Możesz je analizować, modyfikować, translować do SQL, etc.

// Struktura expression tree:
// squareExpr.Body:  BinaryExpression (Multiply)
//   Left:  ParameterExpression (x)
//   Right: ParameterExpression (x)

Console.WriteLine(squareExpr.Body);
// Output: (x * x)

// Możesz też compile i execute
Func<int, int> compiled = squareExpr.Compile();
int result2 = compiled(5);  // 25

Po co Expression Trees? LINQ to EF!

// LINQ to EF używa Expression Trees!
using var db = new MyDbContext();

// To jest Expression<Func<Person, bool>>, nie Func!
var adults = db.People.Where(p => p.Age >= 18);

// EF analizuje expression tree i generuje SQL!
// Expression tree:
// p => p.Age >= 18
// BinaryExpression (GreaterThanOrEqual)
//   Left:  MemberExpression (p.Age)
//   Right: ConstantExpression (18)

// EF translates to SQL:
// SELECT * FROM People WHERE Age >= 18

// Bez Expression Trees, EF nie mógłby translate do SQL!

Building Expression Trees programmatically

// Możesz budować Expression Trees programmatically
// Zamiast: x => x * x

// Ręczne budowanie:
ParameterExpression param = Expression.Parameter(typeof(int), "x");
BinaryExpression multiply = Expression.Multiply(param, param);
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(multiply, param);

// lambda reprezentuje: x => x * x

// Compile i execute
Func<int, int> compiled = lambda.Compile();
int result = compiled(5);  // 25

// Po co? Dynamic query building!
Expression<Func<Person, bool>> BuildFilter(string? city, int? minAge)
{
    ParameterExpression param = Expression.Parameter(typeof(Person), "p");
    Expression filter = Expression.Constant(true);  // Start z true
    
    if (city != null)
    {
        // p.City == city
        var cityProp = Expression.Property(param, nameof(Person.City));
        var cityConst = Expression.Constant(city);
        var cityEquals = Expression.Equal(cityProp, cityConst);
        
        filter = Expression.AndAlso(filter, cityEquals);
    }
    
    if (minAge != null)
    {
        // p.Age >= minAge
        var ageProp = Expression.Property(param, nameof(Person.Age));
        var ageConst = Expression.Constant(minAge.Value);
        var ageCheck = Expression.GreaterThanOrEqual(ageProp, ageConst);
        
        filter = Expression.AndAlso(filter, ageCheck);
    }
    
    return Expression.Lambda<Func<Person, bool>>(filter, param);
}

// Dynamic query!
var filterExpr = BuildFilter("Warsaw", 18);
var query = db.People.Where(filterExpr);
// Generated SQL będzie zawierać tylko używane filters!

Expression Trees - use cases

// Use case 1: LINQ providers (EF, MongoDB, etc.)
var query = db.Products
    .Where(p => p.Price > 100 && p.Category == "Electronics")
    .OrderBy(p => p.Name);
// Expression trees są translated do SQL/MongoDB query

// Use case 2: Dynamic filtering
Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, object value)
{
    var param = Expression.Parameter(typeof(T), "x");
    var property = Expression.Property(param, propertyName);
    var constant = Expression.Constant(value);
    var equals = Expression.Equal(property, constant);
    
    return Expression.Lambda<Func<T, bool>>(equals, param);
}

var predicate = BuildPredicate<Person>("Name", "Jan");
var people = db.People.Where(predicate);  // WHERE Name = 'Jan'

// Use case 3: Validation frameworks
// FluentValidation używa Expression Trees do define rules
RuleFor(customer => customer.Email).NotEmpty();
// Expression tree pozwala FluentValidation analyze property access

// Use case 4: Mapping frameworks (AutoMapper)
// Expression trees do define mappings
CreateMap<Source, Dest>()
    .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FirstName + " " + src.LastName));
Podsumowanie

Delegates, Events i Lambda - functional programming w C#!

  • Delegates - type-safe function pointers, callbacks, multicast
  • Action<T> - void methods (do 16 parametrów)
  • Func<T, TResult> - methods z return value
  • Predicate<T> - boolean checks (legacy)
  • Events - EventHandler, publisher/subscriber pattern
  • Lambda expressions - wszystkie formy, closures
  • C# 9 - Static lambdas - performance optimization
  • 🔥 C# 10 - Natural type - var z lambda, explicit return types, attributes
  • 🔥 C# 14 - ref/out/in/params - parameter modifiers w lambdach!
  • Expression Trees - lambda jako data, LINQ providers, dynamic queries

W kolejnym wpisie skupimy się na atrybutach i refleksji - podstawy atrybutów oraz refleksji, wbudowane atrybuty i wiele, wiele więcej!

Zadanie dla Ciebie 🎯

Stwórz event-driven order processing system:

public class OrderProcessor
{
    // Events
    public event EventHandler<OrderEventArgs> OrderReceived;
    public event EventHandler<OrderEventArgs> OrderValidated;
    public event EventHandler<OrderEventArgs> PaymentProcessed;
    public event EventHandler<OrderEventArgs> OrderShipped;
    public event EventHandler<ErrorEventArgs> ErrorOccurred;
    
    public async Task ProcessOrderAsync(Order order)
    {
        // Raise events na każdym etapie
        // Handle errors i raise ErrorOccurred
    }
}

// Użycie z multiple subscribers:
var processor = new OrderProcessor();
processor.OrderReceived += LogToDatabase;
processor.OrderReceived += SendEmailNotification;
processor.OrderReceived += UpdateInventory;
processor.ErrorOccurred += LogError;
processor.ErrorOccurred += NotifyAdmin;

Wymagania:

  1. Events dla każdego stage procesu
  2. EventArgs z order details + timestamp
  3. Error handling z ErrorOccurred event
  4. Multiple subscribers dla każdego eventu
  5. Async event handlers support
🎯 BONUS: Dynamic Query Builder z Expression Trees

Stwórz advanced query builder używając Expression Trees!

Do zaimplementowania:

  1. QueryBuilder<T> class:
    • Fluent API do budowania predicates
    • Build Expression<Func<T, bool>> dynamically
    • Support multiple conditions (AND/OR)
  2. Operations:
    • Equal, NotEqual, GreaterThan, LessThan
    • Contains, StartsWith, EndsWith (dla string)
    • Between (range checks)
    • In (list of values)
  3. Composite predicates:
    • And() - łączenie z AND
    • Or() - łączenie z OR
    • Not() - negacja
  4. Features:
    • Type-safe (compile-time checking)
    • Works z LINQ to Objects i LINQ to EF
    • ToString() shows readable expression

Przykład użycia:

var builder = new QueryBuilder<Person>();

// Build dynamic predicate
var predicate = builder
    .Where(p => p.Age)
        .GreaterThan(18)
    .And(p => p.City)
        .In("Warsaw", "Krakow", "Gdansk")
    .And(p => p.Name)
        .StartsWith("J")
    .Build();

// Use with LINQ to Objects
var people = GetPeople().Where(predicate.Compile()).ToList();

// Use with LINQ to EF
var dbPeople = db.People.Where(predicate).ToList();

// Generated expression:
// p => (p.Age > 18) && (p.City in ["Warsaw", "Krakow", "Gdansk"]) && p.Name.StartsWith("J")

// Advanced: Dynamic field selection at runtime
string field = GetFieldFromUserInput();  // "Age", "City", etc.
var dynamicPredicate = builder
    .Where(field)
        .GreaterThan(18)
    .Build();

// API endpoint example:
// GET /api/people?age.gt=18&city.in=Warsaw,Krakow&name.startsWith=J
var query = ParseQueryString(Request.QueryString);
var predicate = builder.BuildFromQuery(query);
var results = db.People.Where(predicate).ToList();

Ten projekt demonstruje advanced Expression Trees - dynamic query building, LINQ providers, type-safe API! 🚀🌳