Local functions

C# 7.0 NETFx 4.7

Published Updated Author Jeffrey T. Fritz Reading time

Local functions are methods defined inside other methods or properties, letting you encapsulate helper logic at the point of use. They reduce namespace clutter, improve readability by keeping related logic together, and naturally capture variables from their enclosing scope.

Local functions arrived in C# 7.0. They are scoped to the containing method and cannot be called from outside it. Unlike lambdas stored in delegates, local functions can be recursive, support all parameter and return type features, and integrate seamlessly with async/await and iterators. They close over local variables, similar to closures, but with the structure of traditional methods.

Why it matters

Local functions keep helper methods visible only where they’re needed. Instead of creating a separate private method (polluting the class interface) or a lambda (less readable for complex logic), local functions strike a balance: they’re structured, recursive-capable, and scoped. This is especially valuable for validation logic, recursive algorithms, or multi-step procedures that belong to one method.

Practical examples

Validation within a method:

public void ProcessOrder(Order order)
{
    ValidateOrder(order);
    ShipOrder(order);

    void ValidateOrder(Order o)
    {
        if (o.Items.Count == 0) throw new ArgumentException("Order empty");
        if (o.Total < 0) throw new ArgumentException("Negative total");
    }
}

Recursive tree traversal:

public int CalculateDepth(TreeNode root)
{
    return CalculateDepthLocal(root);

    int CalculateDepthLocal(TreeNode node)
    {
        if (node == null) return 0;
        return 1 + Math.Max(
            CalculateDepthLocal(node.Left),
            CalculateDepthLocal(node.Right)
        );
    }
}

Capturing outer scope:

public decimal ApplyDiscount(decimal price, int customerTier)
{
    decimal discountRate = customerTier switch { 1 => 0.05m, 2 => 0.10m, _ => 0 };

    return CalculateFinalPrice(price);

    decimal CalculateFinalPrice(decimal p) => p * (1 - discountRate);
}

Cautions

Local functions are visible and callable only within their containing method; they cannot be passed as delegate parameters without wrapping in a lambda. They do not appear in reflection or IntelliSense outside their scope, which is intentional encapsulation but can surprise developers unfamiliar with the feature. Performance is equivalent to private methods; the compiler generates hidden private methods at the class level.

Basic local functions

Define helper functions inside methods to encapsulate logic and reduce pollution of class scope.

Valid since C# 7.0

// Before: Separate private methods (clutters class interface)
public class OrderProcessorOld
{
    public void ProcessOrder(Order order)
    {
        ValidateOrderInternal(order);
        CalculateTotal(order);
        ShipOrder(order);
    }

    private void ValidateOrderInternal(Order order)
    {
        if (order == null) throw new ArgumentNullException(nameof(order));
        if (order.Items.Count == 0) throw new ArgumentException("Order is empty");
        if (order.Items.Any(item => item.Quantity <= 0))
            throw new ArgumentException("Invalid quantity");
    }

    private void CalculateTotal(Order order)
    {
        order.Total = order.Items.Sum(item => item.Price * item.Quantity);
    }
}

// After: Local functions (encapsulated at point of use)
public class OrderProcessorNew
{
    public void ProcessOrder(Order order)
    {
        ValidateOrder(order);
        CalculateTotal(order);
        ShipOrder(order);

        // Local validation function - visible only to ProcessOrder
        void ValidateOrder(Order o)
        {
            if (o == null) throw new ArgumentNullException(nameof(o));
            if (o.Items.Count == 0) throw new ArgumentException("Order is empty");
            if (o.Items.Any(item => item.Quantity <= 0))
                throw new ArgumentException("Invalid quantity");
        }

        // Local calculation function
        void CalculateTotal(Order o)
        {
            o.Total = o.Items.Sum(item => item.Price * item.Quantity);
        }
    }

    private void ShipOrder(Order order) { /* ... */ }
}

public class Order
{
    public List<OrderItem> Items { get; set; } = new();
    public decimal Total { get; set; }
}

public class OrderItem
{
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

Local functions with recursion and closures

Use local functions for recursive algorithms and to capture variables from the enclosing scope.

Valid since C# 7.0

public class LocalFunctionsAdvanced
{
    // Recursive local function
    public long Factorial(int n)
    {
        if (n < 0) throw new ArgumentException("n must be non-negative");

        return FactorialLocal(n);

        long FactorialLocal(int x) => x <= 1 ? 1 : x * FactorialLocal(x - 1);
    }

    // Fibonacci with memoization using local function
    public int Fibonacci(int n)
    {
        var cache = new Dictionary<int, int>();

        return FibLocal(n);

        int FibLocal(int x)
        {
            if (x <= 1) return x;
            if (cache.TryGetValue(x, out int cached)) return cached;

            int result = FibLocal(x - 1) + FibLocal(x - 2);
            cache[x] = result;
            return result;
        }
    }

    // Local functions capturing outer variables (closure)
    public Func<int, int> CreateMultiplier(int factor)
    {
        // Local function captures 'factor' from enclosing scope
        int Multiply(int x) => x * factor;

        // Return as delegate
        return Multiply;
    }

    // Multiple local functions with interdependencies
    public void GenerateReport(List<int> data)
    {
        var sum = 0;
        var max = int.MinValue;

        ProcessData();
        PrintSummary();

        void ProcessData()
        {
            foreach (var value in data)
            {
                sum += value;
                if (value > max) max = value;
            }
        }

        void PrintSummary()
        {
            double average = data.Count > 0 ? (double)sum / data.Count : 0;
            Console.WriteLine($"Sum: {sum}, Average: {average:F2}, Max: {max}");
        }
    }

    // TreeNode for recursive example
    public class TreeNode
    {
        public int Value { get; set; }
        public TreeNode Left { get; set; }
        public TreeNode Right { get; set; }
    }

    // Binary tree traversal using local recursion
    public List<int> InOrderTraversal(TreeNode root)
    {
        var result = new List<int>();

        Traverse(root);
        return result;

        void Traverse(TreeNode node)
        {
            if (node == null) return;

            Traverse(node.Left);
            result.Add(node.Value);
            Traverse(node.Right);
        }
    }
}

Local async functions and iterators

Combine local functions with async/await and yield to create powerful iterative and asynchronous patterns.

Valid since C# 7.0

using System;
using System.Collections.Generic;

public class LocalFunctionsAsyncAndIterators
{
    // Local async function
    public async System.Threading.Tasks.Task<string> FetchDataAsync(string url)
    {
        var client = new System.Net.Http.HttpClient();
        var data = await FetchInternal();
        return data;

        async System.Threading.Tasks.Task<string> FetchInternal()
        {
            var response = await client.GetAsync(url);
            return await response.Content.ReadAsStringAsync();
        }
    }

    // Local iterator function (generator)
    public IEnumerable<int> GenerateSequence(int count)
    {
        return GetNumbers();

        IEnumerable<int> GetNumbers()
        {
            for (int i = 0; i < count; i++)
            {
                yield return i * 2;
            }
        }
    }

    // Local async iterator (C# 8.0+)
    public async System.Collections.Generic.IAsyncEnumerable<int> FetchNumbersAsync(int count)
    {
        await foreach (var num in GetNumbersAsync())
        {
            yield return num;
        }

        async IAsyncEnumerable<int> GetNumbersAsync()
        {
            for (int i = 0; i < count; i++)
            {
                await System.Threading.Tasks.Task.Delay(100); // Simulate async work
                yield return i;
            }
        }
    }

    // Local function with lazy evaluation
    public IEnumerable<T> FilterAndTransform<T>(IEnumerable<T> source, Func<T, bool> predicate, Func<T, T> transform)
    {
        return ExecuteTransform();

        IEnumerable<T> ExecuteTransform()
        {
            foreach (var item in source)
            {
                if (predicate(item))
                {
                    yield return transform(item);
                }
            }
        }
    }

    // Combining multiple local functions with async
    public async System.Threading.Tasks.Task<(int success, int failed)> ProcessItemsAsync(string[] items)
    {
        int successCount = 0;
        int failCount = 0;

        foreach (var item in items)
        {
            if (await TryProcess(item))
                successCount++;
            else
                failCount++;
        }

        return (successCount, failCount);

        async System.Threading.Tasks.Task<bool> TryProcess(string item)
        {
            try
            {
                await ProcessItemAsync(item);
                return true;
            }
            catch
            {
                return false;
            }
        }

        async System.Threading.Tasks.Task ProcessItemAsync(string item)
        {
            await System.Threading.Tasks.Task.Delay(50); // Simulate work
            Console.WriteLine($"Processed: {item}");
        }
    }

    // Local async function with resource management
    public async System.Threading.Tasks.Task<int> CountLinesAsync(string filePath)
    {
        return await CountLines();

        async System.Threading.Tasks.Task<int> CountLines()
        {
            int count = 0;
            using (var reader = new System.IO.StreamReader(filePath))
            {
                string line;
                while ((line = await reader.ReadLineAsync()) != null)
                {
                    if (!string.IsNullOrWhiteSpace(line))
                        count++;
                }
            }
            return count;
        }
    }
}

Learn more

Local functions (Microsoft Learn)