TIL: Livable Code. Living with your mess

These days, I watched a conference online by Sarah Mei (@sarahmei) titled: Livable Code. This is what livable code is.

Livable Code is the analogy that building software is like living an space inside a building instead of constructing a house or a building.

Building software isn’t like building anything else. It isn’t like planning, constructing and maintaining a tower of apartments. It’s living an space where each decision counts towards having a mess or a enjoyable place to live.

Code hoarders

We don’t end up with a messy unlivable code because of a bad choice of frameworks or technologies. Of course, the shape of the space will influence the design of our house. But, messy code comes after small decisions made every day. Patch after patch. One day you receive a package and throw the box and the plastic bags in a corner. The next thing you know is that pile almost reaches the ceiling.

A nice pile of junk
Messy code comes after small decisions made every day. Photo by Lance Grandahl on Unsplash

Everyday Decisions and Perfect Clean Code

We have to live with every decision we have made inside our code. Instead of, building something and moving on, we live with software. We don’t have “done” projects anymore. We don’t ship software into burned CD’s and hand the maintenance to somebody else. We have to get to live with our code and make it livable for the ones around us.

There is no perfect clean code. In fact, it’s unattainable. Perfect clean code only exists on books and blog posts. The same as perfectly design minimalist spaces in magazines. We need certain amount of stuff to live. A pile of books, a video game console. An amount of stuff we can tolerate. We could have spaghetti code that nobody understands or so clean and perfectly crafted code that nobody understands either. Both extremes are unlivable.

4 rules for more livable code

The author in the conference gives 4 rules to make your code more livable.

  1. Don’t make it worse. No matter what you have to do, don’t make it worse. Remind this to other team members when needed.
  2. Improvement over consistency. Ship your technical debt away, one change at a time.
  3. Inline everyday. Don’t have stories for big refactors. Make refactoring an everyday job. Always leave the campground cleaner than you found it.
  4. Liase. Communicate, don’t lie. Treat refactoring as everything you do.
    • Don’t ask for permission. But, be upfront.
    • Don’t ask for forgiveness. But, learn every time.
    • Ask for advice. But, don’t always take it.
    • Work together. We all have to live here!

The most important part of software is not the code, or the people. It’s the system.

Happy code living!

A quick guide to LINQ with examples

Today a friend asked me about LINQ. She was studying for a technical interview. So, dear Alice: This is what LINQ is and these are the most common LINQ methods with examples. All you need to know about LINQ in 15 minutes or less.

Language-Integrated Query (LINQ) is the declarative way of working with collections in C#. LINQ works with databases and XML files too. Apart from extensions methods on the IEnumerable type, LINQ has a query syntax, a SQL-like syntax

1. LINQ is declarative

It means we write our code stating the results we want instead of doing every step to get those results.

With LINQ, we write code to “filter a collection based on a condition.” Instead of writing code to “grab an element, check if it satisfies a condition, then move to the next element, check again…“, etc.

LINQ is a better alternative to query collections using for, foreach, or any other loop. With LINQ, we write more expressive and compact code.

Waiting at a cinema before a movie starts
Photo by Erik Witsoe on Unsplash

2. Let’s find our favorite movies

Let’s start with the collection of movies we have watched. We have a Movie class with a name, release year, and rating. Let’s find our favorite movies, the ones with a rating greater than 4.5.

Here’s a Console application that prints our favorite movies,

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var favorites = new List<Movie>();
foreach (var movie in movies)
// ^^^^
{
    if (movie.Rating > 4.5)
    //  ^^^^^
    {
        favorites.Add(movie);
    }
}

Console.WriteLine("My favorites:");
PrintMovies(favorites);
// Output:
// My favorites:
// The Fifth Element: [4.6]
// Terminator 2: [4.7]
// Avatar: [5]
// My Neighbor Totoro: [5]

static void PrintMovies(IEnumerable<Movie> movies)
{
    foreach (var movie in movies)
    {
        Console.WriteLine($"{movie.Name}: [{movie.Rating}]");
    }
}

record Movie(string Name, int ReleaseYear, float Rating);

We wrote a foreach loop and an if statement to find movies with a rating greater than 4.5. No LINQ so far!

Also, we used Top-level statements, records and Global usings from recent C# versions. That’s why we didn’t write the Main class and import the System.Linq namespace.

3. Let’s use our first LINQ method: Where

To work with LINQ, we need to be comfortable with delegates and lambda functions.

In a few words: a delegate is a pointer to a method. And a lambda function is a method with only the parameters and the body. C# has two built-in delegates: Func and Action.

How to filter a collection with Where

If we want to filter our list of movies to keep only those with a rating greater than 4.5, the LINQ method to filter collections is Where().

Where returns a new collection with only the elements that meet a condition.

Let’s replace our foreach loop with the Where() method. And let’s use the condition inside the if statement as the filter condition for Where(). Like this,

var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1997, 4.6f),
    new Movie("Terminator 2", 1991, 4.7f),
    new Movie("Avatar", 2009, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var favorites = movies.Where(movie => movie.Rating > 4.5);
//                     ^^^^^

Console.WriteLine("My favorites:");
PrintMovies(favorites);
// Output:
// My favorites:
// The Fifth Element: [4.6]
// Terminator 2: [4.7]
// Avatar: [5]
// My Neighbor Totoro: [5]

static void PrintMovies(IEnumerable<Movie> movies)
{
    foreach (var movie in movies)
    {
        Console.WriteLine($"{movie.Name}: [{movie.Rating}]");
    }
}

record Movie(string Name, int ReleaseYear, float Rating);

We replaced the foreach and if statements with a single line of code:

var favorites = movies.Where(movie => movie.Rating > 4.5);

More compact, isn’t it? Also, we turned the condition inside the if statement into a lambda function. This is why we need to be comfortable working with delegates.

Where() returned a new collection. It didn’t remove any elements from the original movies list.

LINQ methods don’t change the original collection. They return a result without modifying the original one.

Where accepts a method too

Instead of lambda functions, we can use a method as the filtering condition.

To replace the lambda function from our previous example, let’s create an IsFavorite() method that receives Movie as a parameter and returns bool. For example,

private bool IsFavorite(Movie movie)
  => movie.Rating > 4.5;

Then, we can use IsFavorite() inside the Where() method to filter our movies. Like this,

var favorites = movies.Where(movie => IsFavorite(movie));

Since IsFavorite() only has one parameter, we can remove the parameter name, like this,

var favorites = movies.Where(IsFavorite);

Way more compact and readable than our original version with a foreach and if.

4. Most common LINQ methods

So far, we’ve seen only one LINQ method: Where(). Of course, LINQ has more methods.

Here are the most common LINQ methods:

1. Select

Select transforms a collection by applying a mapping function to every element.

Let’s find only the names of our favorite movies.

var favorites = movies.Where(movie => movie.Rating > 4.5)
                      .Select(movie => movie.Name);

foreach (var name in favorites)
{
    Console.WriteLine(name);
}
// Output:
// The Fifth Element
// Terminator 2
// Avatar
// My Neighbor Totoro

This time we wrote two nested LINQ methods. For every favorite movie, we only picked only its name. Here, the “mapping” function was the delegate: movie => movie.Name.

For more readability, we often align the nested LINQ methods vertically by the (.) dot.

2. Any

Any checks if a collection is empty or has at least one element matching a condition. It doesn’t return a new collection, but either true or false.

Let’s see if we have watched movies with a low rating.

var hasAnyMovies = movies.Any();
// true

var hasBadMovies = movies.Any(movie => movie.Rating < 2);
// false

3. All

Unlike Any, All checks if every element inside a collection matches a condition. It also returns either true or false instead of a new collection.

Let’s see if we have only watched really-good movies.

var weHaveSeenReallyGoodMovies = movies.All(movie => movie.Rating >= 4.5);
// false

4. GroupBy

GroupBy groups the elements of a collection based on a key. It returns a collection of “groups” or “buckets” organized by a key.

Let’s group our movies by rating.

var groupedByRating = movies.GroupBy(movie => movie.Rating);

foreach (var group in groupedByRating)
{
    Console.WriteLine($"Rating: {group.Key}");

    foreach (var movie in group)
    {
        Console.WriteLine($"{movie.Name}");
    }
    Console.WriteLine();
}
// Output:
// Rating: 4.5
// Titanic
//
// Rating: 4.6
// The Fifth Element
//
// Rating: 4.7
// Terminator 2
//
// Rating: 5
// Avatar
// My Neighbor Totoro
//
// Rating: 4
// Platoon

We grouped our list of movies using only one property: Rating. But, GroupBy has other use-cases: transforming each group and grouping by more than one property.

5. First and FirstOrDefault

First and FirstOrDefault return the first element in a collection or the first one matching a condition. First throws an exception if the collection is empty or doesn’t have matching elements. And FirstOrDefault returns the default value of the element type, instead.

Let’s find the oldest film we have watched.

var oldest = movies.OrderBy(movie => movie.ReleaseYear)
                   .First();
// Platoon

Here we first used OrderBy() to sort the movie collection by release year and then picked the first one.

In the same spirit of First() and FirstOrDefault(), we have Last() and LastOrDefault(). They return the last element instead of the first one.

.NET6 introduced new LINQ methods and oveloads. FirstOrDefault() and similar XOrDefault() methods have a new overload to pass an optional default value. And, we have methods like MinBy() we can use to replace an OrderBy() followed by First().

If we peek into the source code of DistinctBy, one of those new LINQ methods, we will see it’s not that intimidating after all.

5. Cheatsheet

There are more LINQ methods than the ones we’ve seen so far. These are some of them.

Method Function
Where Filter a collection
Select Transform every element of a collection
Any Check if a collection is empty
All Check if every element satisfies a condition
Count Count all elements of a collection
Distinct Find the unique elements of a collection
GroupBy Group the elements of a collection based on a key
OrderBy Sort a collection based on a key
First Find the first element of a collection. Throw if the collection is empty
FirstOrDefault Same as First but it returns a default value if it’s empty
Last Find the last element of a collection. Throw if the collection is empty
LastOrDefault It returns a default value if it’s empty, instead
Single Find only one element in a collection matching a condition. Throw, otherwise
SingleOrDefault It returns a default value if there isn’t one matching element, instead
Take Pick the first n consecutive elements of a collection
TakeWhile Pick the first consecutive elements that satisfy a condition
Skip Return a collection without the first n consecutive elements
SkipWhile Return a collection without the first consecutive elements that satisfy a condition
Sum Sum the elements of a collection
Min, Max Find the smallest and largest element of a collection
ToDictionary Convert a collection into a dictionary

That isn’t an exhaustive list. Of course, LINQ has more methods we don’t use often, like Aggregate and Intersect, Union, and Except. They’re helpful from time to time.

Popcorn
Speaking of movies. Photo by Christian Wiediger on Unsplash

6. LINQ Method syntax vs Query syntax

Up to this point, we have seen LINQ as extension methods on top of the IEnumerable type. But, LINQ has a language-level query syntax too.

Let’s find our favorite movies using language-level query syntax this time. Like this,

var bestOfAll = from movie in movies
                where movie.Rating > 4.5
                select movie;

It looks like SQL, isn’t it?

And this is the same code using extension methods,

var bestOfAll = movies.Where(movie => movie.Rating > 4.5);

We can use any of the two! But let’s favor the syntax used in the codebase we’re working with. If our code uses extension methods on IEnumerable, let’s continue to do that.

But there is one advantage of using query syntax over extension methods: we can create intermediate variables with the let keyword.

Find large files on the Desktop folder

Let’s find all files inside our Desktop folder larger than 10MB. And, let’s use let to create a variable. Like this,

var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
var desktop = new DirectoryInfo(desktopPath);

var largeFiles = from file in desktop.GetFiles()
                 let sizeInMb = file.Length / 1024 / 1024
                 //  ^^^^^
                 // We can create intermediate variables with 'let'
                 where sizeInMb > 10
                 select file.Name;

foreach (var file in largeFiles)
{
    Console.WriteLine(file);
}

Console.ReadKey();

The Length property returns the file size in bytes. We declared an intermediate variable to convert it to megabytes. Like this,

let sizeInMb = file.Length / 1024 / 1024

That’s the advantage of using query syntax over extension methods when working with LINQ.

7. Three common LINQ mistakes

It’s easy to start using LINQ. But, it’s easy to use misuse some of its methods. Here are three common mistakes when using LINQ:

1. Write Count instead of Any

Let’s always prefer Any() over Count() to check if a collection has elements or an element that meets a condition.

Let’s do,

movies.Any()

Instead of,

movies.Count() > 0

2. Write Where followed by Any

Let’s use a condition with Any() instead of filtering first with Where() to later use Any().

Let’s do,

movies.Any(movie => movie.Rating == 5)

Instead of,

movies.Where(movie => movie.Rating == 5).Any()

The same applies to the Where() method followed by FirstOrDefault(), Count(), or any other method that receives a filter condition.

3. Use FirstOrDefault without null checking

Let’s always check the result when working with FirstOrDefault(), LastOrDefault(), and SingleOrDefault(). If there isn’t one, they will return the default value of the collection type.

// We don't have movies with a rating lower than 2.0
var movies = new List<Movie>
{
    new Movie("Titanic", 1998, 4.5f),
    new Movie("The Fifth Element", 1995, 4.6f),
    new Movie("Terminator 2", 1999, 4.7f),
    new Movie("Avatar", 2010, 5),
    new Movie("Platoon", 1986, 4),
    new Movie("My Neighbor Totoro", 1988, 5)
};

var worst = movies.FirstOrDefault(movie => movie.Rating < 2);

Console.WriteLine($"{worst.Name}: [{worst.Rating}]");
//                  ^^^^^^^^^^^^ 
// System.NullReferenceException: 'Object reference not set to an instance of an object.'
//
// worst was null.

record Movie(string Name, int ReleaseYear, float Rating);

For objects, the default value would be a null reference. And do you know what happens when we try to access a property or method on a null reference?… Yes, it throws NullReferenceException.

To make sure we always have a non-nullable result when working with FirstOrDefault(), let’s use the the DefaultIfEmpty method. It returns a new collection with a default value if the input collection is empty.

var worst = movies.Where(movie => movie.Rating < 2)
                  .DefaultIfEmpty(new Movie("Catwoman", 2004, 3))
                  // ^^^^^
                  .First();

Also, by mistake, we forget about the difference between Single and First and LINQ lazy evaluation expecting LINQ queries to be cached. These two are more subtle mistakes we make when working with LINQ for the first time.

8. Conclusion

Voilà! That’s it, Alice. That’s all you need to know to start working with LINQ in your code in 15 minutes or less. I know! There are lots of methods. But you will get your back covered with five of the most common LINQ methods.

With LINQ, we write more compact and expressive code. The next time we need to write logic using loops, let’s give LINQ a try!

Want to write more expressive code for collections? Join my course, Getting Started with LINQ on Udemy and learn everything you need to know to start working productively with LINQ—in less than 2 hours.

Happy LINQ time!

TIL: Three differences between TRUNCATE and DELETE

Both DELETE and TRUNCATE remove records from a table. But, these days I learned three differences between TRUNCATE and DELETE statements in SQL Server. Let me share them with you.

DELETE accepts a WHERE condition to only remove some records, TRUNCATE doesn’t. DELETE doesn’t reset identity columns to its initial value, but TRUNCATE does. And, DELETE fire triggers, TRUNCATE doesn’t.

To see these three differences in action, let’s create a sample database with a Movies table. It only contains an auto-incremented id, a movie title and an score.

CREATE DATABASE DeleteVsTruncate
GO

USE DeleteVsTruncate
GO

CREATE TABLE dbo.Movies(
    Id INT PRIMARY KEY IDENTITY,
    Name VARCHAR(255) NOT NULL,
    Score INT
)
GO

INSERT INTO dbo.Movies
VALUES
    ('Titanic', 5),
    ('The Fifth Element', 5),
    ('Terminator 2', 5)
GO

1. TRUNCATE doesn’t accept a WHERE clause

The first difference is about the WHERE clause.

DELETE accepts a WHERE clause to only delete some records from a table. But, TRUNCATE doesn’t. It deletes all records from a table.

If you try to add a WHERE clause with TRUNCATE, you get “Incorrect syntax near the keyword ‘WHERE’“.

SELECT * FROM dbo.Movies
GO

DELETE FROM dbo.Movies WHERE Name = 'Armageddon'
GO
TRUNCATE TABLE dbo.Movies WHERE Name = 'Armageddon'
                          // ^^^^^^^ Incorrect syntax near the keyword 'WHERE'

2. TRUNCATE resets Identity columns

An identity column is a column with automatic incremented values. It’s used to create key values in tables.

Values for identity columns start from a “seed” value and increase by an “increment” value. You can use any number as seed and any positive or negative number as increment. By default, if you don’t use any seed or increment, it starts from 1 and increments by 1. IDENTITY = IDENTITY(1, 1)

DELETE statements don’t reset identity columns. It means new rows will have the next value in the identity columns. But, TRUNCATE does reset identity columns. The next new row will have the seed in the identity column.

Let’s delete all movies from our sample table and see the Id columns for the new movies.

SELECT * FROM dbo.Movies
GO
DELETE FROM dbo.Movies
GO
INSERT INTO dbo.Movies
VALUES
    ('Avatar', 5)
GO
SELECT * FROM dbo.Movies
GO
DELETE doesn't reset identity columns

Notice how ‘Avatar’ still has the Id = 4 after deleting all movies.

Now, let’s see how TRUNCATE resets the identity column. This time, let’s use TRUNCATE instead of DELETE.

SELECT * FROM dbo.Movies
GO
TRUNCATE TABLE dbo.Movies
GO
INSERT INTO dbo.Movies
VALUES
    ('Platoon', 4)
GO
SELECT * FROM dbo.Movies
GO
TRUNCATE resets identity columns

Notice the Id of ‘Platoon’. It’s 1 again. When we created our Movies table, we used the default seed and increment.

3. TRUNCATE doesn’t fire Triggers

A trigger is an special type of store procedure that runs when a given action has happened at the database or table level.

For example, you can run a custom action inside a trigger after INSERT, DELETE or UPDATE to a table.

When you work with triggers, you have two virtual tables: INSERTED and DELETED. These tables hold the values inserted or deleted in the statement that fired the trigger in the first place.

Now, back to the differences between TRUNCATE and DELETE. DELETE fires triggers, TRUNCATE doesn’t.

Let’s create a trigger that shows the deleted values. It uses the DELETED table.

CREATE OR ALTER TRIGGER dbo.PrintDeletedMovies
ON dbo.Movies
AFTER DELETE
AS
BEGIN
    SELECT Id 'Deleted Id', Name 'Deleted Name', Score 'Deleted Score'
    FROM DELETED;
END

Now, let’s delete our movies with DELETE and TRUNCATE to see what happens. First, let’s add some new movies and let’s use the DELETE.

INSERT INTO dbo.Movies
VALUES
    ('Titanic', 5),
    ('The Fifth Element', 5),
    ('Terminator 2', 5)
GO
DELETE FROM dbo.Movies
GO
DELETE fires triggers

Notice the results tab with our three sample movies. Now, let’s use TRUNCATE.

INSERT INTO dbo.Movies
VALUES
    ('Titanic', 5),
    ('The Fifth Element', 5),
    ('Terminator 2', 5)
GO
TRUNCATE TABLE dbo.Movies
GO
TRUNCATE doesn't fire triggers

Now, after truncating the table, we only see in the messages tab the number of rows affected. No movies shown.

Bonus: Rollback a TRUNCATE

We can rollback a TRUNCATE operation. To see this, let’s add our three movies and use a ROLLBACK with some SELECT’s in between.

INSERT INTO dbo.Movies
VALUES
    ('Titanic', 5),
    ('The Fifth Element', 5),
    ('Terminator 2', 5)
GO

BEGIN TRAN
    TRUNCATE TABLE dbo.Movies;

    SELECT * FROM dbo.Movies;
ROLLBACK
GO

SELECT * FROM dbo.Movies;
GO
You can rollback a TRUNCATE

Notice, the two results. The one inside the transaction, before the rollback, is empty. And the last one, after the rollback, with our three movies.

Voilà! Those are three differences between DELETE and TRUNCATE.

For more content about SQL Server, check how to write Dynamic SQL and Two free tools to format your SQL queries.

A case of primitive obsession. A real example in C#

These days I was working with Stripe API to take payments. And I found a case of primitive obsession. Keep reading to learn how to get rid of it.

Primitive obsession is when developers choose primitive types (strings, integers, decimals) to represent entities of the business domain. To solve this code smell, create classes to model the business entities and to enforce the appropriate business rules.

Using Stripe API

Stripe API uses units to represent amounts. All amounts are multiplied by 100. This is 1USD = 100 units. Also, we can only use amounts between $0.50 USD and $999,999.99 USD. This isn’t the case for all currencies, but let’s keep it simple. For more information, check Stripe documentation for currencies.

The codebase I was working with used two extension methods on the decimal type to convert between amounts and units. Those two methods were something like ToUnits() and ToAmount().

But, besides variable names, there wasn’t anything preventing me to use a decimal instead of Stripe units. It was the same decimal type for both concepts. Anyone could forget to convert things and charge someone’s credit card more than expected. Arggg!

A case of primitive obsession
Photo by rupixen.com on Unsplash

Getting rid of primitive obsession

1. Create a type alias

As an alternative to encode units of measure on variable names, we can use a type alias.

Let’s declare a new type alias with using Unit = System.Decimal and change the correct parameters to use Unit. But, the compiler won’t warn us if we pass decimal instead of Unit. See the snippet below.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GettingRidOfPrimitiveObsession
{
    using Unit = System.Decimal;

    [TestClass]
    public class ConvertBetweenAmountAndUnits
    {
        [TestMethod]
        public void UseTypeAlias()
        {
            decimal amount = 100;
            Unit chargeAmount = amount.ToUnit();

            var paymentService = new PaymentService();
            paymentService.Pay(chargeAmount);

            paymentService.Pay(amount);
            // ^^^^ It compiles
        }
    }

    public class PaymentService
    {
        public void Pay(Unit amountToCharge)
        {
            // Magic goes here
        }
    }

    public static class DecimalExtensions
    {
        public static Unit ToUnits(this decimal d)
            => d * 100;
    }
}

Using a type alias is more expressive than encoding the unit of measure in variable names and parameters. But, it doesn’t force us to use one type instead of the other.

With the Unit type alias, we can still, by mistake, pass regular decimals when we meant units.

Let’s try a better alternative!

2. Create a new type

Now, let’s create a Unit class and pass it around instead of decimal.

In the constructor of the new Unit class, let’s check if the input amount is inside Stripe bounds. Also, let’s use a method to convert units back to normal amounts.

public class Unit
{
    internal decimal Value { get; }

    private Unit(decimal d)
    {
        if (d < 0.5m || d > 999_999.99m)
        {
            throw new ArgumentException("Amount outside of bounds");
        }

        Value = d * 100m;
    }

    public static Unit FromAmount(decimal d)
      => new Unit(d);

    public decimal ToAmount()
        => Value / 100m;
}

Notice, we made the constructor private and added a FromAmount() factory method for more readability.

After using a class instead of an alias, the compiler will warn us if we switch the two types by mistake. And, it’s clear from a method signature if it works with amounts or units.

[TestMethod]
public void UseAType()
{
    decimal amount = 100;
    Unit chargeAmount = Unit.FromAmount(amount);

    var paymentService = new PaymentService();
    paymentService.Pay(chargeAmount);

    // paymentService.Pay(amount);
    // ^^^^ cannot convert from 'decimal' to 'GettingRidOfPrimitiveObsession.Unit'
}

If needed, we can overload the + and - operators to make sure we’re not adding oranges and apples. Decimals and units, I mean.

Records from C# 9.0 offer a shorter notation for classes to replace primitive values. Records have built-in memberwise comparison, ToString() methods and copy constructors, among other features.

We can use custom classes, like the Unit class we wrote, to encode restrictions, constraints, and business rules in our business domain. That’s the main takeaway from Domain Modeling Made Functional.

Voilà! That’s how we can get rid of primitive obsession. A type alias was more expressive than encoding units of measure on names. But, a class was a better alternative. By the way, F# supports unit of measures to variables. And, the compiler will warn you if you forget to use the right unit of measure.

Looking for more content on C#? Check my post series on C# idioms and my C# definitive guide. Working with Stripe, too? Check my post on how to use the Decorator pattern to implement retry logic.

Happy coding!

TIL: Always Use a Culture When Parsing Numeric Strings in C#

This week, I reinstalled the operating system of my computer. The new version uses Spanish instead of English. After that, some unit tests started to break in one of my projects. The broken tests verified the formatting of currencies. This is what I learned about parsing numeric strings and unit testing.

To have a set of always-passing unit tests, use a default culture when parsing numeric strings. Add a default culture to the Parse() and ToString() methods on decimals. As an alternative, wrap each test in a method to change the current culture during its execution.

Failing to parse numeric strings

Some of the failing tests looked like the one below. These tests verified the separator for each supported currency in the project.

[TestMethod]
public void ToCurrency_IntegerAmount_FormatsAmountWithTwoDecimalPlaces()
{
    decimal value = 10M;
    var result = value.ToCurrency();

    Assert.AreEqual("10.00", result);
}

And this was the ToCurrency() method.

public static string ToCurrency(this decimal amount)
{
    return amount.ToString("0.00");
}

The ToCurrency() method didn’t specify any culture. Therefore, it used the user’s current culture. And, the tests expected . as the separator for decimal places. That wasn’t the case for the culture I started to use after reinstalling my operating system. It was ,. That’s why those tests failed.

Use a default culture when parsing

To make my failing tests always pass, no matter the culture, I added a default culture when parsing numeric strings.

Always add a default culture when parsing numeric strings.

For example, you can create ToCurrency() and FromCurrency() methods like this:

public static class FormattingExtensions
{
    private static CultureInfo DefaultCulture
        = new CultureInfo("en-US");

    public static string ToCurrency(this decimal amount)
    {
        return amount.ToString("0.00", DefaultCulture);
    }

    public static decimal FromCurrency(this string amount)
    {
        return decimal.Parse(amount, DefaultCulture);
    }
}

Notice that I added a second parameter of type CultureInfo, which defaults to “en-US.”

Alternatively: Use a wrapper in your tests

As an alternative to adding a default culture, I could run each test inside a wrapper that changes the user culture to the one needed and revert it when the test finishes.

Something like this,

static string RunInCulture(CultureInfo culture, Func<string> action)
{
    var originalCulture = Thread.CurrentThread.CurrentCulture;
    Thread.CurrentThread.CurrentCulture = culture;
    
    try
    {
        return action();
    }
    finally
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
    }
}

Then, I could refactor the tests to use the RunInCulture wrapper method, like this,

private readonly CultureInfo DefaultCulture
  = new CultureInfo("en-US");

[TestMethod]
public void ToCurrency_IntegerAmount_FormatsAmountWithTwoDecimalPlaces()
{
    RunInCulture(DefaultCulture, () =>
    {
        decimal value = 10M;
        var result = value.ToCurrency();

        Assert.AreEqual("10.00", result);
    });
}

Voilà! That’s what I learned after reinstalling my computer’s operating system and running some unit tests. I learned to use a default culture in all of my parsing methods. If you change your computer locale, all your tests continue to pass?

If you’re new to unit testing, read Unit Testing 101, 4 common mistakes when writing unit tests and 4 test naming conventions. Don’t miss the rest of my Unit Testing 101 series where I also cover mocking, assertions, and best practices.

Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.

All tests turned green!