Let's say you need to implement a feature that returns a different package based on the user-provided coupon code.

So you start with a model:

public record Package
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

And you write a function that returns a different package based on the coupon code:

private static Package GetPackageFromCoupon(string coupon)
{
    if (coupon == "ABC")
    {
        return new Package { Id = 1, Name = "PS5 Controller", Price = 50.00 };
    }
    if (coupon == "EBC")
    {
        return new Package { Id = 2, Name = "Iphone X", Price = 200.00 };
    }
    if (coupon == "DDD")
    {
        return new Package { Id = 3, Name = "X7 Mouse", Price = 20.00 };
    }
    return new Package
    {
        Id = 1000,
        Name = "Soda",
        Price = 1.00
    };
}

Enter fullscreen mode Exit fullscreen mode

And invoke it from your main method:

internal class Program
{
    static void Main(string[] args)
    {
        var package = GetPackageFromCoupon("ABC");
        Console.WriteLine(package);
        Console.ReadLine();

    }
}

Enter fullscreen mode Exit fullscreen mode

Quick test

Provide expected parameters and inspect the results.

"ABC" => Package { Id = 1, Name = PS5 Controller, Price = 50 }
"EBC" => Package { Id = 2, Name = Iphone X, Price = 200 }
"DDD" => Package { Id = 3, Name = X7 Mouse, Price = 20 }

Enter fullscreen mode Exit fullscreen mode

Works as expected. Also, if you enter something that doesn't exist:

"a" => Package { Id = 1000, Name = Soda, Price = 1 }

Enter fullscreen mode Exit fullscreen mode

The Problem

What if you need to add more coupon codes and return different variations of the Package object? Well, it's gonna get pretty messy very soon.

Quick solution - Dictionary

Rather than writing every possible variation in the if block, create a dictionary where the key is the coupon code and the value is the Package:

private static readonly Dictionary<string, Package> _packages = new()
{
    ["ABC"] = new Package { Id = 1, Name = "PS5 Controller", Price = 50.00 },
    ["EBC"] = new Package { Id = 2, Name = "Iphone X", Price = 200.00 },
    ["DDD"] = new Package { Id = 3, Name = "X7 Mouse", Price = 20.00 },
};

Enter fullscreen mode Exit fullscreen mode

The next step is to enter the coupon code and either read the value from the dictionary or return a default value.

private static Package GetPackageFromCoupon(string coupon)
{
    var isExistingPackage = _packages.TryGetValue(coupon, out var package);

    if (isExistingPackage)
    {
        return package;
    }

    return new Package
    {
        Id = 1000,
        Name = "Soda",
        Price = 1.00
    };
}

Enter fullscreen mode Exit fullscreen mode

Run the program, and you'll get back the same result!

But why a dictionary? Why not an array or a list? The answer is Big O notation. Reading an item by key is immensely faster than querying a collection. Dictionary lookup is O(1), while list search is O(n).

Let's spice things up a bit.

What if the output varies?

Change request comes in. You need to introduce seasons. If it's the holiday season, the price should be lower.

You need to create a function that takes the season as a parameter and adjusts the price accordingly. To keep it simple, add static methods in the Package class:

public record Package
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }

    public static Package PS5Controller(bool isHolidaySeason)
    {
        return new Package { 
            Id = 1, 
            Name = "PS5 Controller", 
            Price = isHolidaySeason ? 20.00 : 50.00
        };
    }

    public static Package IphoneX(bool isHolidaySeason)
    {
        return new Package
        {
            Id = 2,
            Name = "Iphone X",
            Price = isHolidaySeason ? 50.00 : 200.00
        };
    }

    public static Package X7Mouse(bool isHolidaySeason)
    {
        return new Package
        {
            Id = 3,
            Name = "X7 Mouse",
            Price = isHolidaySeason ? 10.00 : 20.00
        };
    }

    public static Package DefaultPackage(bool isHolidaySeason)
    {
        return new Package
        {
            Id = 1000,
            Name = "Soda",
            Price = isHolidaySeason ? 0.00 : 1.00
        };
    }

}

Enter fullscreen mode Exit fullscreen mode

But how do you call this function the dictionary? How do you pass that parameter?

private static readonly Dictionary<string, Package> _packages = new()
{
    ["ABC"] = Package.PS5Controller(), // ?
};

Enter fullscreen mode Exit fullscreen mode

The Func Delegate

The Func<T, TResult> is a built-in generic delegate that encapsulates a method (that takes between 0 and 16 input parameters) and returns a value.

So instead of Dictionary<string, Package>, create a
Dictionary<string, Func<bool, Package>> where the bool is the input parameter (holiday season) and the Package is the output (method that returns a particular package).

private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
{
    ["ABC"] = (isHoliday) => Package.PS5Controller(isHoliday),
    ["EBC"] = (isHoliday) => Package.IphoneX(isHoliday),
    ["DDD"] = (isHoliday) => Package.X7Mouse(isHoliday),
};

Enter fullscreen mode Exit fullscreen mode

This can be further simplified:

private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
{
    ["ABC"] = Package.PS5Controller,
    ["EBC"] = Package.IphoneX,
    ["DDD"] = Package.X7Mouse,
};

Enter fullscreen mode Exit fullscreen mode

This works because C# allows method groups to be assigned to delegates when the signatures match.

The last piece of the puzzle is to adapt the GetPackageFromCoupon() method to also accept the holiday season:

private static Package GetPackageFromCoupon(string coupon, bool isHoliday)
{
    var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);

    if (isExistingPackage)
    {
        return packageFactory(isHoliday);
    }

    return Package.DefaultPackage(isHoliday);
}

Enter fullscreen mode Exit fullscreen mode

Clarifications

This time around, the Dictionary returns a function (using a coupon code):

var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);

Enter fullscreen mode Exit fullscreen mode

The function is invoked by passing the isHoliday parameter:

return packageFactory(isHoliday);

Enter fullscreen mode Exit fullscreen mode

The packageFactory() is a pointer to the function returned from the Dictionary. It can be any of the three specified functions.

Lastly, if there is no match, return a default value:

return Package.DefaultPackage(isHoliday);

Enter fullscreen mode Exit fullscreen mode

Final test

Invoke the function with different variants:

static void Main(string[] args)
{

    var isHolidaySeason = true;
    var package = GetPackageFromCoupon("ABC", isHolidaySeason);

    Console.WriteLine(package);
    Console.ReadLine();

}

Enter fullscreen mode Exit fullscreen mode

  • During the holiday season:
Package { Id = 1, Name = PS5 Controller, Price = 20 }

Enter fullscreen mode Exit fullscreen mode

  • When not:
Package { Id = 1, Name = PS5 Controller, Price = 50 }

Enter fullscreen mode Exit fullscreen mode

This also works for the default scenario:

var isHolidaySeason = true;
var package = GetPackageFromCoupon("aaaa", isHolidaySeason);

// Package { Id = 1000, Name = Soda, Price = 0 }

Enter fullscreen mode Exit fullscreen mode

Full Example (Program.cs)

using CSharpPractice.Coupons;

namespace CSharpPractice
{
    internal class Program
    {
        static void Main(string[] args)
        {

            var isHolidaySeason = true;
            var package = GetPackageFromCoupon("ABC", isHolidaySeason);

            Console.WriteLine(package);
            Console.ReadLine();
        }

        private static Package GetPackageFromCoupon(string coupon, bool isHoliday)
        {
            var isExistingPackage = _packages.TryGetValue(coupon, out var packageFactory);

            if (isExistingPackage)
            {
                return packageFactory(isHoliday);
            }

            return Package.DefaultPackage(isHoliday);
        }

        private static readonly Dictionary<string, Func<bool, Package>> _packages = new()
        {
            ["ABC"] = Package.PS5Controller,
            ["EBC"] = Package.IphoneX,
            ["DDD"] = Package.X7Mouse,
        };


    }

}

Enter fullscreen mode Exit fullscreen mode

Summary

This approach also aligns with the Open–Closed Principle: the code is open for extension (add new coupon codes or pricing rules) but closed for modification (no need to touch existing logic).
Adding a new coupon doesn't require rewriting method conditions. Just one dictionary entry.

Until next time 👋