You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Async & Functional Patterns

Master asynchronous programming, delegates, and error handling strategies

Async & Functional Patterns in C#

Master asynchronous programming and functional patterns in C# with free flashcards and spaced repetition practice. This lesson covers async/await mechanics, Task-based Asynchronous Pattern (TAP), functional programming concepts like immutability and higher-order functions, and LINQ for declarative data manipulationโ€”essential skills for writing modern, efficient C# applications.

Welcome

Welcome to the world of asynchronous and functional programming in C#! ๐Ÿ’ป Modern applications demand responsiveness and efficiency, whether you're building web APIs, desktop apps, or mobile solutions. This lesson bridges two powerful paradigms: async programming for handling I/O-bound operations without blocking threads, and functional patterns for writing cleaner, more maintainable code through immutability and pure functions.

๐ŸŽฏ What You'll Learn:

  • How async/await transforms asynchronous code
  • Task management and concurrency patterns
  • Functional programming principles in C#
  • LINQ for powerful data transformations
  • Combining async and functional approaches

Core Concepts

Understanding Async/Await ๐Ÿ”„

The async/await keywords revolutionized asynchronous programming in C# by making async code look and behave like synchronous code. Before async/await, developers struggled with callback hell and complex state machines.

How It Works:

Synchronous Execution          Asynchronous Execution
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€          โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ Method  โ”‚                    โ”‚ Method  โ”‚
    โ”‚ Starts  โ”‚                    โ”‚ Starts  โ”‚
    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜                    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
         โ”‚                              โ”‚
         โ–ผ                              โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚  I/O    โ”‚ โ—„โ”€ BLOCKS          โ”‚ await   โ”‚ โ—„โ”€ YIELDS
    โ”‚Operationโ”‚    THREAD          โ”‚  Task   โ”‚    THREAD
    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜                    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
         โ”‚                              โ”‚ (thread free)
         โ”‚ (thread busy)                โ”‚ (other work)
         โ–ผ                              โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚Continue โ”‚                    โ”‚Continue โ”‚
    โ”‚ Code    โ”‚                    โ”‚  Code   โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The async Keyword:

  • Marks a method as asynchronous
  • Enables use of await inside the method
  • Changes method return type: Task, Task<T>, or ValueTask<T>

The await Keyword:

  • Suspends method execution until the awaited task completes
  • Returns control to the caller (frees the thread)
  • Captures execution context for continuation

๐Ÿ’ก Key Insight: await doesn't create a new threadโ€”it releases the current thread back to the thread pool, improving scalability!

Task-Based Asynchronous Pattern (TAP) โšก

TAP is the recommended pattern for async operations in .NET, built on the Task and Task<T> types.

Task Characteristics:

AspectDescriptionExample
Hot vs ColdTasks start immediately (hot)Task.Run(() => Work())
Result AccessUse .Result or awaitvar data = await task;
StatusCreated, Running, Completed, Faulted, Canceledtask.Status
CompositionCombine with WhenAll, WhenAnyawait Task.WhenAll(tasks)

Common Task Operations:

// Task.Run - Execute work on thread pool
var task = Task.Run(() => ExpensiveComputation());

// Task.Delay - Asynchronous wait
await Task.Delay(1000); // Wait 1 second without blocking

// Task.WhenAll - Wait for multiple tasks
var tasks = new[] { DownloadAsync(url1), DownloadAsync(url2) };
var results = await Task.WhenAll(tasks);

// Task.WhenAny - Wait for first completion
var completedTask = await Task.WhenAny(tasks);

// Task.FromResult - Create completed task
return Task.FromResult(42);

โš ๏ธ Common Mistake: Blocking on async code with .Result or .Wait() can cause deadlocks in UI and ASP.NET contexts!

ConfigureAwait and Context ๐ŸŽฏ

When you await a task, by default the continuation runs on the captured synchronization context (UI thread, ASP.NET request context, etc.).

// Captures context - continuation runs on original context
await SomeTaskAsync();

// Does NOT capture context - continuation runs on thread pool
await SomeTaskAsync().ConfigureAwait(false);

๐Ÿ’ก Best Practice: Use .ConfigureAwait(false) in library code to improve performance and avoid deadlocks. Only capture context when you need it (updating UI, accessing request data).

Functional Programming Principles ๐Ÿงฎ

Functional programming emphasizes immutability, pure functions, and declarative code. C# isn't purely functional, but it supports many functional patterns.

Core Concepts:

๐Ÿ“‹ Functional Programming Pillars

ImmutabilityData cannot be modified after creation
Pure FunctionsSame input always produces same output, no side effects
First-Class FunctionsFunctions as values (parameters, return values)
Higher-Order FunctionsFunctions that take or return functions
Declarative StyleExpress WHAT, not HOW

Immutability in C#:

// Mutable (imperative)
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var person = new Person { Name = "Alice", Age = 30 };
person.Age = 31; // Mutation

// Immutable (functional)
public record Person(string Name, int Age);

var person = new Person("Alice", 30);
var olderPerson = person with { Age = 31 }; // Creates new instance

Pure Functions:

// Impure - has side effects, depends on external state
private int counter = 0;
public int IncrementCounter()
{
    counter++; // Modifies state
    Console.WriteLine(counter); // Side effect
    return counter;
}

// Pure - no side effects, deterministic
public int Add(int a, int b)
{
    return a + b; // Always same output for same input
}

๐Ÿ’ก Why Pure Functions? They're easier to test, reason about, parallelize, and cache (memoization).

Higher-Order Functions and Delegates ๐ŸŽญ

C# treats functions as first-class citizens through delegates, lambdas, and Func/Action types.

// Func<T, TResult> - function with return value
Func<int, int> square = x => x * x;
var result = square(5); // 25

// Action<T> - function with no return value
Action<string> log = message => Console.WriteLine(message);
log("Hello");

// Higher-order function - takes function as parameter
public static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (predicate(item))
            result.Add(item);
    }
    return result;
}

// Usage
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evens = Filter(numbers, n => n % 2 == 0); // [2, 4, 6]

Function Composition:

// Compose two functions
public static Func<T, TResult> Compose<T, TIntermediate, TResult>(
    Func<T, TIntermediate> f,
    Func<TIntermediate, TResult> g)
{
    return x => g(f(x));
}

Func<int, int> addOne = x => x + 1;
Func<int, int> double = x => x * 2;
var addOneThenDouble = Compose(addOne, double);
var result = addOneThenDouble(5); // (5 + 1) * 2 = 12

LINQ: Language Integrated Query ๐Ÿ”

LINQ brings functional query capabilities to C#, enabling declarative data manipulation.

LINQ Query Operators:

CategoryOperatorsPurpose
FilteringWhere, OfTypeSelect items matching criteria
ProjectionSelect, SelectManyTransform items
OrderingOrderBy, ThenBySort sequences
GroupingGroupBy, ToLookupGroup items by key
AggregationCount, Sum, AverageCompute values
SetDistinct, Union, IntersectSet operations
PartitioningTake, Skip, ChunkSelect subsets

LINQ Syntax Styles:

var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Method syntax (fluent)
var evenSquares = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * n)
    .ToList(); // [4, 16, 36, 64, 100]

// Query syntax (SQL-like)
var evenSquares2 = (from n in numbers
                    where n % 2 == 0
                    select n * n)
                   .ToList();

๐Ÿ’ก Performance Tip: LINQ uses deferred executionโ€”queries aren't executed until enumerated. Chain operations before materializing with ToList(), ToArray(), etc.

Combining Async and Functional Patterns ๐Ÿš€

The real power comes from combining async and functional approaches for clean, efficient code.

Async LINQ with IAsyncEnumerable:

// Async stream processing
public async IAsyncEnumerable<string> FetchPagesAsync(string[] urls)
{
    foreach (var url in urls)
    {
        var content = await httpClient.GetStringAsync(url);
        yield return content;
    }
}

// Consume async stream
await foreach (var page in FetchPagesAsync(urls))
{
    ProcessPage(page);
}

Parallel Async Operations:

// Sequential - slow (each waits for previous)
var results = new List<Data>();
foreach (var id in ids)
{
    var data = await FetchDataAsync(id);
    results.Add(data);
}

// Parallel - fast (all run concurrently)
var tasks = ids.Select(id => FetchDataAsync(id));
var results = await Task.WhenAll(tasks);

// Parallel with LINQ transformation
var processedData = await Task.WhenAll(
    ids.Select(async id => 
    {
        var data = await FetchDataAsync(id);
        return ProcessData(data);
    })
);

Option/Maybe Pattern:

// Functional error handling without exceptions
public record Option<T>
{
    private readonly T? _value;
    private readonly bool _hasValue;

    private Option(T? value, bool hasValue)
    {
        _value = value;
        _hasValue = hasValue;
    }

    public static Option<T> Some(T value) => new(value, true);
    public static Option<T> None() => new(default, false);

    public TResult Match<TResult>(
        Func<T, TResult> some,
        Func<TResult> none) =>
        _hasValue ? some(_value!) : none();
}

// Async option usage
public async Task<Option<User>> FindUserAsync(int id)
{
    var user = await database.Users.FindAsync(id);
    return user != null ? Option<User>.Some(user) : Option<User>.None();
}

var result = await FindUserAsync(42);
var message = result.Match(
    some: user => $"Found: {user.Name}",
    none: () => "User not found"
);

Railway-Oriented Programming (Result Pattern):

public record Result<T>
{
    public T? Value { get; init; }
    public string? Error { get; init; }
    public bool IsSuccess => Error == null;

    public static Result<T> Success(T value) => 
        new() { Value = value };
    public static Result<T> Failure(string error) => 
        new() { Error = error };
}

// Chain operations that might fail
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
{
    var validationResult = ValidateRequest(request);
    if (!validationResult.IsSuccess)
        return Result<Order>.Failure(validationResult.Error!);

    var inventoryResult = await CheckInventoryAsync(request.Items);
    if (!inventoryResult.IsSuccess)
        return Result<Order>.Failure(inventoryResult.Error!);

    var order = await CreateOrderAsync(request);
    return Result<Order>.Success(order);
}

Examples

Example 1: Async Web Data Aggregation

Scenario: Fetch data from multiple APIs and combine results efficiently.

public class DataAggregator
{
    private readonly HttpClient _httpClient;

    public async Task<AggregatedData> FetchDataAsync(string userId)
    {
        // Start all requests concurrently
        var profileTask = FetchProfileAsync(userId);
        var ordersTask = FetchOrdersAsync(userId);
        var preferencesTask = FetchPreferencesAsync(userId);

        // Wait for all to complete
        await Task.WhenAll(profileTask, ordersTask, preferencesTask);

        // Combine results functionally
        return new AggregatedData(
            Profile: await profileTask,
            Orders: await ordersTask,
            Preferences: await preferencesTask
        );
    }

    private async Task<Profile> FetchProfileAsync(string userId)
    {
        var json = await _httpClient
            .GetStringAsync($"https://api.example.com/users/{userId}")
            .ConfigureAwait(false);
        return JsonSerializer.Deserialize<Profile>(json)!;
    }

    private async Task<List<Order>> FetchOrdersAsync(string userId)
    {
        var json = await _httpClient
            .GetStringAsync($"https://api.example.com/orders?user={userId}")
            .ConfigureAwait(false);
        return JsonSerializer.Deserialize<List<Order>>(json)!;
    }

    private async Task<UserPreferences> FetchPreferencesAsync(string userId)
    {
        var json = await _httpClient
            .GetStringAsync($"https://api.example.com/preferences/{userId}")
            .ConfigureAwait(false);
        return JsonSerializer.Deserialize<UserPreferences>(json)!;
    }
}

public record AggregatedData(
    Profile Profile,
    List<Order> Orders,
    UserPreferences Preferences
);

Key Points:

  • ๐Ÿ”„ Three API calls run concurrently, not sequentially
  • โšก Total time โ‰ˆ slowest request (not sum of all requests)
  • ๐Ÿ“ฆ record types provide immutability
  • ๐ŸŽฏ .ConfigureAwait(false) for library code

Example 2: Functional Data Transformation Pipeline

Scenario: Process and analyze sales data using LINQ.

public class SalesAnalyzer
{
    public record Sale(DateTime Date, string Product, decimal Amount, string Region);
    public record ProductSummary(string Product, int Count, decimal Total, decimal Average);

    public List<ProductSummary> AnalyzeSales(List<Sale> sales, int year)
    {
        return sales
            // Filter to target year
            .Where(s => s.Date.Year == year)
            // Filter out invalid amounts
            .Where(s => s.Amount > 0)
            // Group by product
            .GroupBy(s => s.Product)
            // Transform to summary
            .Select(g => new ProductSummary(
                Product: g.Key,
                Count: g.Count(),
                Total: g.Sum(s => s.Amount),
                Average: g.Average(s => s.Amount)
            ))
            // Order by total descending
            .OrderByDescending(p => p.Total)
            // Materialize
            .ToList();
    }

    public Dictionary<string, decimal> TopProductsByRegion(
        List<Sale> sales, 
        int topN = 3)
    {
        return sales
            .GroupBy(s => s.Region)
            .ToDictionary(
                g => g.Key,
                g => g.GroupBy(s => s.Product)
                      .Select(p => new { Product = p.Key, Total = p.Sum(s => s.Amount) })
                      .OrderByDescending(p => p.Total)
                      .Take(topN)
                      .Sum(p => p.Total)
            );
    }
}

Key Points:

  • ๐Ÿ”— Method chaining creates readable pipeline
  • ๐Ÿ“Š Declarative styleโ€”express what, not how
  • ๐ŸŽฏ Single pass through data for efficiency
  • ๐Ÿงฎ LINQ handles complex aggregations elegantly

Example 3: Retry Pattern with Async and Functional Style

Scenario: Implement resilient API calls with exponential backoff.

public class ResilientHttpClient
{
    private readonly HttpClient _httpClient;

    public async Task<T> GetWithRetryAsync<T>(
        string url,
        int maxRetries = 3,
        int baseDelayMs = 1000)
    {
        return await Retry(
            operation: async () =>
            {
                var response = await _httpClient.GetAsync(url);
                response.EnsureSuccessStatusCode();
                var json = await response.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<T>(json)!;
            },
            maxAttempts: maxRetries,
            delayStrategy: attempt => TimeSpan.FromMilliseconds(
                baseDelayMs * Math.Pow(2, attempt - 1)
            )
        );
    }

    private async Task<T> Retry<T>(
        Func<Task<T>> operation,
        int maxAttempts,
        Func<int, TimeSpan> delayStrategy)
    {
        Exception? lastException = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++)
        {
            try
            {
                return await operation();
            }
            catch (Exception ex) when (attempt < maxAttempts)
            {
                lastException = ex;
                var delay = delayStrategy(attempt);
                await Task.Delay(delay);
            }
        }

        throw new Exception(
            $"Operation failed after {maxAttempts} attempts",
            lastException
        );
    }
}

// Usage
var client = new ResilientHttpClient();
var data = await client.GetWithRetryAsync<UserData>(
    "https://api.example.com/user/123",
    maxRetries: 5,
    baseDelayMs: 500
);

Exponential Backoff Timeline:

Attempt 1: Request โ†’ โŒ Fail
           โ†“ Wait 500ms
Attempt 2: Request โ†’ โŒ Fail
           โ†“ Wait 1000ms (500 ร— 2ยน)
Attempt 3: Request โ†’ โŒ Fail
           โ†“ Wait 2000ms (500 ร— 2ยฒ)
Attempt 4: Request โ†’ โŒ Fail
           โ†“ Wait 4000ms (500 ร— 2ยณ)
Attempt 5: Request โ†’ โœ… Success

Key Points:

  • ๐Ÿ” Higher-order function Retry takes operation as parameter
  • โฑ๏ธ Strategy pattern via delayStrategy function
  • ๐ŸŽฏ Generic implementation works with any return type
  • ๐Ÿ’ช Exponential backoff prevents overwhelming failing services

Example 4: Async Stream Processing with IAsyncEnumerable

Scenario: Process large datasets without loading everything into memory.

public class LogProcessor
{
    // Async generator - yields results as they're ready
    public async IAsyncEnumerable<LogEntry> StreamLogsAsync(
        string filePath,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        using var reader = new StreamReader(filePath);

        while (!reader.EndOfStream)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var line = await reader.ReadLineAsync();
            if (line == null) break;

            if (TryParseLogEntry(line, out var entry))
            {
                yield return entry;
            }
        }
    }

    // Transform async stream functionally
    public async Task<Dictionary<string, int>> CountErrorsByTypeAsync(
        string filePath)
    {
        var errorCounts = new Dictionary<string, int>();

        await foreach (var log in StreamLogsAsync(filePath))
        {
            if (log.Level == LogLevel.Error)
            {
                var errorType = ExtractErrorType(log.Message);
                errorCounts[errorType] = 
                    errorCounts.GetValueOrDefault(errorType) + 1;
            }
        }

        return errorCounts;
    }

    // Process with LINQ-like operations
    public async Task<List<LogEntry>> FindSuspiciousActivityAsync(
        string filePath)
    {
        var suspicious = new List<LogEntry>();

        await foreach (var log in StreamLogsAsync(filePath)
            .Where(l => l.Level >= LogLevel.Warning)
            .Where(l => l.Message.Contains("unauthorized") || 
                       l.Message.Contains("failed login")))
        {
            suspicious.Add(log);
        }

        return suspicious;
    }

    private bool TryParseLogEntry(string line, out LogEntry entry)
    {
        // Parsing logic
        entry = default!;
        return true;
    }

    private string ExtractErrorType(string message)
    {
        // Error type extraction logic
        return "Unknown";
    }
}

public record LogEntry(DateTime Timestamp, LogLevel Level, string Message);
public enum LogLevel { Debug, Info, Warning, Error, Critical }

Key Points:

  • ๐ŸŒŠ Streaming processes one item at a timeโ€”memory efficient
  • ๐Ÿ”„ IAsyncEnumerable<T> enables async iteration with await foreach
  • ๐Ÿ›‘ Cancellation support via CancellationToken
  • ๐Ÿ” LINQ extensions work with async streams (System.Linq.Async)

Common Mistakes

โš ๏ธ Mistake 1: Async Void Methods

// โŒ WRONG - exceptions can't be caught, hard to test
public async void ProcessDataAsync()
{
    await FetchDataAsync();
}

// โœ… RIGHT - return Task for proper error handling
public async Task ProcessDataAsync()
{
    await FetchDataAsync();
}

Exception: Only use async void for event handlers.

โš ๏ธ Mistake 2: Blocking on Async Code

// โŒ WRONG - can cause deadlocks
public void LoadData()
{
    var data = FetchDataAsync().Result; // Blocks!
    var data2 = FetchDataAsync().GetAwaiter().GetResult(); // Also blocks!
}

// โœ… RIGHT - async all the way
public async Task LoadDataAsync()
{
    var data = await FetchDataAsync();
}

โš ๏ธ Mistake 3: Not Awaiting Tasks

// โŒ WRONG - fire and forget, exceptions lost
public void StartProcess()
{
    ProcessDataAsync(); // Task started but not awaited
}

// โœ… RIGHT - await or explicitly handle
public async Task StartProcessAsync()
{
    await ProcessDataAsync();
}

โš ๏ธ Mistake 4: Sequential When Parallel Would Work

// โŒ WRONG - slow, sequential execution
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);
var prefs = await GetPreferencesAsync(id);

// โœ… RIGHT - parallel execution
var tasks = new[]
{
    GetUserAsync(id),
    GetOrdersAsync(id),
    GetPreferencesAsync(id)
};
await Task.WhenAll(tasks);

โš ๏ธ Mistake 5: Capturing Variables in Loops

// โŒ WRONG - closure captures loop variable
for (int i = 0; i < 5; i++)
{
    tasks.Add(Task.Run(() => Console.WriteLine(i))); // May print 5 five times!
}

// โœ… RIGHT - capture local copy
for (int i = 0; i < 5; i++)
{
    int localI = i;
    tasks.Add(Task.Run(() => Console.WriteLine(localI)));
}

โš ๏ธ Mistake 6: Materializing LINQ Too Early

// โŒ WRONG - creates intermediate list
var filtered = data.Where(x => x.IsActive).ToList();
var sorted = filtered.OrderBy(x => x.Name).ToList();
var result = sorted.Take(10).ToList();

// โœ… RIGHT - chain operations, single materialization
var result = data
    .Where(x => x.IsActive)
    .OrderBy(x => x.Name)
    .Take(10)
    .ToList();

โš ๏ธ Mistake 7: Ignoring ConfigureAwait in Libraries

// โŒ WRONG - library code capturing context unnecessarily
public async Task<Data> FetchDataAsync()
{
    return await httpClient.GetAsync(url); // Captures context
}

// โœ… RIGHT - avoid context capture in library code
public async Task<Data> FetchDataAsync()
{
    return await httpClient.GetAsync(url).ConfigureAwait(false);
}

Key Takeaways

๐ŸŽฏ Async Programming:

  • Use async/await for I/O-bound operations to improve scalability
  • await releases threads, doesn't create new ones
  • Always return Task or Task<T>, never async void (except event handlers)
  • Use Task.WhenAll for parallel operations, Task.WhenAny for first completion
  • Apply .ConfigureAwait(false) in library code

๐ŸŽฏ Functional Patterns:

  • Embrace immutability with record types and read-only properties
  • Write pure functions without side effects for testability
  • Use higher-order functions (Func, Action) for composability
  • Leverage LINQ for declarative data transformations
  • Chain operations before materializing results

๐ŸŽฏ Combining Paradigms:

  • IAsyncEnumerable<T> combines async with streaming
  • LINQ works seamlessly with async operations
  • Functional patterns like Option/Result improve error handling
  • Compose small, pure functions for complex workflows

๐ŸŽฏ Best Practices:

  • Avoid blocking on async code with .Result or .Wait()
  • Don't capture loop variables in closures
  • Handle cancellation with CancellationToken
  • Use appropriate patterns: retry, circuit breaker, timeout
  • Profile performanceโ€”async isn't always faster for CPU-bound work

๐Ÿ“‹ Quick Reference Card

PatternSyntaxUse Case
Async Methodasync Task MethodAsync()I/O-bound operations
Await Taskawait taskExpressionWait without blocking
Parallel Tasksawait Task.WhenAll(tasks)Concurrent operations
LINQ Filter.Where(predicate)Select matching items
LINQ Transform.Select(selector)Project to new form
LINQ Group.GroupBy(keySelector)Group by key
Pure Functionint Add(int a, int b) => a + bDeterministic, no side effects
Immutable Recordrecord Person(string Name, int Age)Value-based equality
Higher-OrderFunc predicatePass functions as values
Async StreamIAsyncEnumerableStream large datasets

๐Ÿ“š Further Study

๐ŸŽ“ You now have the tools to write modern, efficient C# code combining the best of async and functional programming!