Skip to main content
SEMastery
.NET Corebeginner

How to Apply Functional Programming in C#: A Beginner's Guide

Learn functional programming in C# the simple way: pure functions, immutability, records, LINQ, pattern matching, and composition with friendly examples.

13 min readUpdated May 16, 2026

Think about making a fresh cup of chai at home. You take the same water, the same milk, the same tea leaves, the same sugar, and the same flame for the same time. Every single time, you get the same cup of chai. The recipe does not secretly change the sugar jar in your neighbour's kitchen. It does not depend on the mood of the weather. Same ingredients in, same chai out.

That simple idea is the heart of functional programming. You build small steps that take some input and give back an answer, and they do not quietly change things around them. In this guide we will learn how to apply this style in C#, step by step, in plain English, with small examples and pictures. By the end you will be able to write code that is easier to test, easier to read, and far less likely to surprise you.

What is functional programming, really?

Functional programming is a way of thinking about code. Instead of telling the computer how to change things one line at a time, you describe what answer you want by combining small functions.

There are three big ideas you need to hold in your head:

  1. Pure functions — functions that are honest. Same input, same output, no hidden surprises.
  2. Immutability — data that does not change after you create it. If you want a change, you make a fresh copy.
  3. Composition — joining small functions together like train coaches to build bigger behaviour.

C# is what we call a multi-paradigm language. That is a fancy way of saying you do not have to choose. You can write objects and classes like you always have, and sprinkle in functional ideas where they help. Modern C# (C# 14, shipping in .NET 10, the current LTS release) gives you records, LINQ, lambdas, and pattern matching that make this style feel natural.

The three pillars of functional thinking in C#.

Pillar one: pure functions

A pure function follows two simple rules:

  • Its answer depends only on the values you pass in.
  • It changes nothing outside itself (no writing to files, no editing global variables, no printing to the screen as its main job).

Let us look at an impure function first, so you can feel the difference.

// Impure: it depends on something outside (the field 'taxRate')
// and that field could change without this method knowing.
public class Billing
{
    public decimal TaxRate = 0.18m;
 
    public decimal AddTax(decimal price)
    {
        return price + (price * TaxRate);
    }
}

The problem is that AddTax(100) might give a different answer tomorrow if someone changes TaxRate. The function is not honest about everything it depends on. Now here is the pure version.

// Pure: everything it needs comes in as a parameter,
// and it changes nothing outside. Same inputs => same output, always.
public static class Billing
{
    public static decimal AddTax(decimal price, decimal taxRate)
    {
        return price + (price * taxRate);
    }
}
 
// AddTax(100m, 0.18m) is ALWAYS 118m. Forever. You can trust it.

Because the pure version takes everything it needs as input, you can read it once and know exactly what it does. You can test it without setting up a database, a clock, or the internet. And because it touches nothing shared, many threads can call it at the same time safely.

How a pure function behaves

Input
Pure function
Output

Steps

1

Input

price and taxRate passed in

2

Pure function

calculates using only inputs

3

Output

same answer every time

Inputs go in, an answer comes out, and the outside world is untouched.

A quick test you can run in your head

Ask yourself two questions about any function:

QuestionIf "yes"If "no"
Does it always return the same output for the same input?Good, stays pureIt is impure
Does it change anything outside itself?It is impureGood, stays pure

A pure function answers "yes" to the first and "no" to the second. The more of your functions you can make pure, the calmer your codebase becomes.

Pillar two: immutability

Immutable means "cannot be changed after it is made." Think of a printed train ticket. Once it is printed, you do not scribble a new seat number on it. If your plans change, you get a fresh ticket. The old one stays exactly as it was.

Why is this useful? When data never changes, you never get those nasty bugs where one part of the program edits a value while another part is still using it. No race conditions. No "who changed this?" mysteries.

C# gives us records, which were added in C# 9 and have grown ever since. A positional record is immutable by default, and it comes with a handy with expression for making a changed copy.

// A record: a small, immutable data holder.
public record Order(string Item, int Quantity, decimal Price);
 
var order = new Order("Notebook", 2, 50m);
 
// We do NOT change 'order'. We make a fresh copy with one field updated.
var biggerOrder = order with { Quantity = 5 };
 
// 'order' still says Quantity = 2. 'biggerOrder' says Quantity = 5.
// The original is safe and untouched.

The with keyword is the immutable way to "change" something. It copies the record, swaps the fields you mention, and leaves the original alone. This is sometimes called non-destructive mutation — you get your update without destroying the old value.

The 'with' expression makes a fresh copy instead of editing in place.

For collections, the same idea applies. Instead of editing a list in place, functional code prefers methods that return a new list. The System.Collections.Immutable package gives you types like ImmutableList<T> that make this safe and clear.

Pillar three: composition

Composition means joining small functions so the output of one becomes the input of the next. Picture a tiffin factory line. One worker adds rice. The next adds dal. The next packs the box. Each worker does one tiny job, and together they make a finished lunch.

In code, you can do the same. Here is a small example using simple steps.

using System;
 
public static class TextPipeline
{
    // Three small, pure steps.
    public static string Trim(string s) => s.Trim();
    public static string Lower(string s) => s.ToLowerInvariant();
    public static string Hyphenate(string s) => s.Replace(' ', '-');
 
    // Compose them into one "make a slug" step.
    public static string MakeSlug(string raw)
    {
        Func<string, string> pipeline = input =>
            Hyphenate(Lower(Trim(input)));
 
        return pipeline(raw);
    }
}
 
// MakeSlug("  Hello World  ") => "hello-world"

Each step is tiny and easy to check on its own. When you compose them, you build bigger behaviour without writing one giant, tangled method. If a step is wrong, you fix that one small step.

Composing small functions into a pipeline

Trim
Lower
Hyphenate

Steps

1

Trim

remove spaces at ends

2

Lower

make all lowercase

3

Hyphenate

spaces become dashes

Each step transforms the value and hands it to the next.

LINQ: functional programming you already use

Here is some good news. If you have ever used LINQ, you have already written functional code without realising it. LINQ is built on the very ideas we just learned: small functions, no changing the original, and chaining steps together.

using System.Linq;
using System.Collections.Generic;
 
var orders = new List<Order>
{
    new("Notebook", 2, 50m),
    new("Pen", 10, 5m),
    new("Bag", 1, 800m)
};
 
// A functional pipeline: filter, transform, and total.
// Notice: 'orders' is never changed. Each step returns something new.
decimal bigSpend = orders
    .Where(o => o.Price >= 50m)        // keep expensive items
    .Select(o => o.Price * o.Quantity) // turn each into a line total
    .Sum();                            // add them up
 
// bigSpend = (50 * 2) + (800 * 1) = 900

Look at what happened. We started with a list. We filtered it, transformed it, and summed it. The original orders list is completely unchanged. Each method takes a small function (a lambda like o => o.Price >= 50m) and returns a new sequence. This is functional thinking in its everyday clothes.

A LINQ query is a pipeline of small, pure steps.

Pattern matching: making decisions the functional way

Functional code loves to look at the shape of data and decide what to do. C# gives us pattern matching and switch expressions for exactly this. Instead of a long ladder of if/else statements that change a variable, you describe each case and return a value.

public record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public record Square(double Side) : Shape;
 
public static class Geometry
{
    // A switch expression: one case per shape, each returns an area.
    public static double Area(Shape shape) => shape switch
    {
        Circle c    => 3.14159 * c.Radius * c.Radius,
        Rectangle r => r.Width * r.Height,
        Square s    => s.Side * s.Side,
        _           => throw new ArgumentException("Unknown shape")
    };
}
 
// Geometry.Area(new Circle(2)) => about 12.566

This reads like a small table of rules. Each arm is a tiny pure expression. There is no shared variable being edited along the way. Records and pattern matching are a perfect pair, because a positional record can be matched by its parts.

StyleHow it decidesFunctional friendliness
if / else ladderRun statements, often change a variableLower, easy to make mistakes
switch expressionMatch the shape, return a valueHigher, clean and pure
Pattern matchingLook at data shape and valuesHigher, very expressive

Avoiding the side effects trap

A side effect is when a function does something beyond returning a value: writing to a file, sending an email, saving to a database, or printing to the screen. Real programs must do these things. So functional style does not ban side effects. It just asks you to push them to the edges.

The idea is simple. Keep the core of your program pure (the calculations and decisions), and do the messy outside-world work (database, network, files) in a thin layer around that core. This keeps most of your code easy to test, with only a small risky border.

Pure core, messy edges

Read input
Pure logic
Write output

Steps

1

Read input

get data at the edge

2

Pure logic

calculate with no side effects

3

Write output

save or print at the edge

Keep calculations pure inside, push side effects to the outer ring.
public static class CheckoutCore
{
    // Pure: no database, no printing. Just a clean decision.
    public static decimal FinalTotal(
        IEnumerable<Order> items, decimal taxRate, decimal discount)
    {
        decimal subtotal = items.Sum(i => i.Price * i.Quantity);
        decimal afterDiscount = subtotal - discount;
        return Billing.AddTax(afterDiscount, taxRate);
    }
}
 
// The database save happens OUTSIDE this method, in your app's edge.
// This pure core can be unit tested in milliseconds, with no setup.

A note on libraries and tools

You may hear about helper libraries for functional C#, such as LanguageExt, which add types for safely handling missing values and errors without throwing exceptions. These are optional. You can apply everything in this guide using plain C# and the standard library.

One honest heads-up while we are talking about the .NET ecosystem: some popular libraries have changed how they are licensed. For example, MediatR and MassTransit are now commercially licensed for many uses. They are not functional-programming tools, but you may meet them in real projects, so it is worth knowing their license terms changed. Always check a library's current license before adding it to your work.

Putting it all together

Let us connect the pieces into one clear mental model. You read data in, you run it through pure functions and pattern matching, you keep your data immutable along the way, and you only touch the outside world at the very start and the very end.

The shape of a small functional C# program.

Here is a tiny end-to-end example that uses every idea: records for immutable data, LINQ for the pipeline, a pure function for the rule, and a switch expression for a decision.

public record Student(string Name, int Marks);
 
public static class Results
{
    // Pure rule: turn marks into a grade. Same marks => same grade.
    public static string Grade(int marks) => marks switch
    {
        >= 90 => "A",
        >= 75 => "B",
        >= 50 => "C",
        _     => "Needs work"
    };
 
    public static IEnumerable<string> TopReport(IEnumerable<Student> students)
        => students
            .Where(s => s.Marks >= 50)              // keep passers
            .OrderByDescending(s => s.Marks)        // best first
            .Select(s => $"{s.Name}: {Grade(s.Marks)}"); // label each
}

Every line is small, honest, and testable. The students collection is never changed. If a teacher asks "why did Ravi get a B?", you can read Grade in two seconds and know the answer.

When should you reach for functional style?

You do not have to rewrite your whole app. Start small and grow. Functional style shines when:

  • You are doing calculations, rules, or data transformations.
  • You want code that is easy to unit test.
  • You are working with data that many parts of the program share.

Keep objects and classes where they help with structure. Use functional ideas for the logic inside. This blend is exactly what modern C# was designed for.

Quick recap

  • Functional programming means building small, honest functions and joining them together, like steps in a chai recipe.
  • A pure function depends only on its inputs and changes nothing outside. Same input, same output, every time.
  • Immutability means data does not change after creation. Use C# records and the with expression to make changed copies safely.
  • Composition joins small functions so the output of one feeds the next, like a tiffin factory line.
  • LINQ is functional programming you already use: Where, Select, and Sum never change the original.
  • Pattern matching and switch expressions let you decide by the shape of data and return a value, instead of editing variables.
  • Push side effects (database, files, network) to the edges, and keep a pure core that is easy to test.
  • You do not have to choose between functional and object-oriented. C# lets you mix both, and that is its strength.

References and further reading

Related Posts