For Cleaner Domains, Move IO to the Edges of Your App

Don’t get too close with I/O.

That’s how I’d summarize the talk “Moving IO to the edges of your app” by Scott Wlaschin at NDC Sydney 2024.

In case you don’t know Scott Wlaschin’s work, he runs the site F# for Fun and Profit and talks about Functional Programming a lot. He’s a frequent speaker at the NDC Conference.

Here’s the YouTube video of the talk, in case you want to watch it:

These are the main takeaways from that talk and how I’d follow them to refactor a piece of code from one of my past projects.

I/O Is Evil: Keep It at Arm’s Length

In a perfect world, all code should be pure. The same inputs return the same outputs with no side effects.

But we’re not in a perfect world, and our code is full of impurities: retrieving the current time, accessing the network, and calling databases.

Instead of aiming for 100% pure code, the guideline is to move I/O (or impurities) away from the business logic or rules.

IO at the edges
Move IO to the Edges. Created based on speaker's slides

When we mix I/O with our domain logic, we make our domain logic harder to understand and test, and more error-prone.

So let’s pay attention to functions with no inputs or no outputs. Often, they do I/O somewhere.

If you think we don’t write functions with no outputs, let’s take another look at our repositories.

Sure, our Create or Update methods might return an ID. But they’re not deterministic. If we insert the same record twice, we get different IDs or even an error if we have unique constraints in our tables.

The guideline here is to write code that is:

  • Comprehensible: it receives what it needs as input and returns some output.
  • Deterministic: it returns the same outputs, given the same input.
  • Free of side effects: it doesn’t do anything under the hood.

Just Return the Decision

This is the example shown in the talk:

Let’s say we need to update a customer’s personal information. If the customer changes their email, we should send a verification email. And, of course, we should update the new name and email in the database.

This is how we might do that,

async static Task UpdateCustomer(Customer newCustomer)
{
    var existing = await CustomerDb.ReadCustomer(newCustomer.Id); // <--

    if (existing.Name != newCustomer.Name
        || existing.EmailAddress != newCustomer.EmailAddress)
    {
        await CustomerDb.UpdateCustomer(newCustomer); // <--
    }
    
    if (existing.EmailAddress != newCustomer.EmailAddress)
    {
        var message = new EmailMessage(newCustomer.EmailAddress, "Some message here...");
        await EmailServer.SendMessage(message); // <--
    }
}

We’re mixing the database calls with our decision-making code. IO is “close” to our business logic.

Of course, we might argue static methods are a bad idea and pass two interfaces instead: ICustomerDb and IEmailServer. But we’re still mixing IO with business logic.

This time, the guideline is to create an imperative shell and just return the decision from our business logic.

Here’s how to update our customers “just returning the decision,”

enum UpdateCustomerDecision
{
    DoNothing,
    UpdateCustomerOnly,
    UpdateCustomerAndSendEmail
}

// This is a good place for discriminated unions.
// But we still don't have them in C#. Sorry!
record UpdateCustomerResult(
    UpdateCustomerDecision Decision,
    Customer? Customer,
    EmailMessage? Message);

static UpdateCustomerResult UpdateCustomer(Customer existing, Customer newCustomer)
{
    var result = new UpdateCustomerResult(UpdateCustomerDecision.DoNothing, null, null);

    if (existing.Name != newCustomer.Name
        || existing.EmailAddress != newCustomer.EmailAddress)
    {
        result = result with
        {
            Decision = UpdateCustomerDecision.UpdateCustomerOnly,
            Customer = newCustomer
        };
    }

    if (existing.EmailAddress != newCustomer.EmailAddress)
    {
        var message = new EmailMessage(newCustomer.EmailAddress, "Some message here...");

        result = result with
        {
            Decision = UpdateCustomerDecision.UpdateCustomerAndSendEmail,
            Message = message
        };
    }

    return result;
}

async static Task ImperativeShell(Customer newCustomer)
{
    var existing = await CustomerDb.ReadCustomer(newCustomer.Id);

    var result = UpdateCustomer(existing, newCustomer);
    //           ^^^^^
    // Nothing impure here

    switch (result.Decision)
    {
        case DoNothing:
            // Well, doing nothing...
            break;

        case UpdateCustomerOnly:
            // Updating the database here...
            break;

        case UpdateCustomerAndSendEmail:
            // Update the database here...
            // And, send the email here...
            break;
    }
}

With the imperative shell, we don’t have to deal with database calls and email logic inside our UpdateCustomer(). And we can unit test it without mocks.

As a side note, UpdateCustomerDecision and UpdateCustomerResult are a simple alternative to discriminated unions. Think of discriminated unions like enums where each member could be an object of a different type.

In more complex codebases, ImperativeShell() would be like a use case class or command handler.

Pure Code Doesn’t Talk to the Outside

When we push I/O to the edges, our pure code doesn’t need exception handling or asynchronous logic. Our pure code doesn’t talk to the outside world.

These are the three code smells the speaker shared to watch out for in our domain code:

  1. Is it async? If so, you’re doing I/O somewhere
  2. Is it catching exceptions? Again, you’re (probably) doing I/O somewhere
  3. Is it throwing exceptions? Why not use a proper return value?

If any of these are true, we’re doing IO inside our domain. And we should refactor our code. “All hands man your refactoring stations.”

Moving I/O to the Edges When Sending an Email

While watching this talk, I realized I could refactor some code I wrote for sending emails in a past project.

Before sending an email, we need to validate if we’re sending it to valid domains. And, after calling a third-party email service, we should store a tracking number and update the email status. Something like this,

public class Email
{
    // Imagine more properties like From, Subject, Body here...
    private readonly IEnumerable<Recipient> _recipients = new List<Recipient>();

    public async Task SendAsync(
        IEmailService emailService,
        IDomainValidationService validationService,
        CancellationToken cancellationToken)
    {
        try
        {
            await validationService.ValidateAsync(this, cancellationToken);

            // It assumes that ValidateAsync changes the recipient's status
            if (_recipients.Any(t => t.LastStatus != DeliveryStatus.FailedOnSend))
            {
                var trackingId = await emailService.SendEmailAsync(this, cancellationToken);
                SetTrackingId(trackingId);
                MarkAsSentToProvider();
            }
        }
        catch (Exception ex)
        {
            UpdateStatus(DeliveryStatus.FailedOnSend);
            throw new SendEmailException("Sending email failed.", ex);
        }
    }
}

But this code contains the three code smells we should avoid: it has asynchronous logic and throws and catches exceptions, and even our Domain is aware of cancellation tokens. Arrggg!

That was an attempt to do Domain Driven Design (DDD) at a past team. And probably, our team at that time picked those conventions from the book Hands-on Domain-Driven Design with .NET Core.

And the imperative shell that calls SendAsync() is something like this,

public class SendEmailHandler : IEventHandler<EmailCreatedEvent>
{
    // Imagine some fields and a constructor here...

    public async Task Handle(EmailCreatedEvent evt, CancellationToken cancellationToken)
    {
        var email = await _emailRepository.GetByIdAsync(evt.EmailId);
                        ?? throw new EmailNotFoundException(evt.EmailId);

        try
        {
            await email.SendAsync(_emailService, _validationService, cancellationToken);

            await _emailRepository.UpdateAsync(email, cancellationToken);
        }
        catch (Exception ex)
        {
            email.SetFailedOnSend(ex.Message);
            await _emailRepository.UpdateAsync(email, cancellationToken);
        }
    }
}

And here’s the same logic “returning the decision,”

// This is a poor man's discriminated union
public abstract record SendingAttempt
{
    private SendingAttempt() { }

    public record SentToSome(Guid TrackingId, IEnumerable<Recipient> Recipients) : SendingAttempt;
    public record SentToNone() : SendingAttempt;
    public record FailedToSend(string Message): SendingAttempt;
}

public class Email
{
    // Imagine more properties like From, Subject, Body here...
    private readonly IEnumerable<Recipient> _recipients = new List<Recipient>();

    public Email Send(SendingAttempt attempt)
    {
        switch (attempt)
        {
            case SendingAttempt.SentToSome:
                // Set trackingId and mark as Sent for some recipients
		// Mark all other recipients as Invalid
		break;
            
            case SendingAttempt.SentToNone:
		// Mark all recipients as Invalid
		break;

            case SendingAttempt.FailedToSend:
                // Mark all recipients as Failed
		break;
        }
    }
}

In this refactored version, we’ve removed the asynchronous logic and exception handling. Now, it receives a SendingAttempt with the result of validating domains and email delivery to the email provider.

Also, it doesn’t have any dependencies passed as interfaces. It embraces Dependency Rejection.

And here’s the imperative shell,

public class SendEmailHandler : IEventHandler<EmailCreatedEvent>
{
    // Imagine some fields and a constructor here...

    public async Task Handle(EmailCreatedEvent evt, CancellationToken cancellationToken)
    {
        var email = await _emailRepository.GetByIdAsync(evt.EmailId)
                        ?? throw new EmailNotFoundException(evt.EmailId);

        var result = await _validationService.ValidateAsync(email, cancellationToken);
        
        // Use result to find valid and invalid destinations...
	// Attempt to send email and catch any exceptions...
        var sendingAttempt = BuildASendingAttemptHere();

        email.Send(sendingAttempt);
	//    ^^^^
	// Nothing impure here

        await _emailRepository.UpdateAsync(email, cancellationToken);
    }
}

Now, the imperative shell validates email domains and tries to send the email, encapsulating all the I/O around Send(). After this refactoring, we should rename Send() inside our domain to something else.

Voila! That’s one approach to have pure business logic, not the one and only approach.

Whether we follow Ports and Adapters, Clean Architecture, or Functional Core-Imperative Shell, the goal is to abstract dependencies and avoid “contaminating” our business domain.

For more content on architecture and modeling, check Domain Modeling Made Functional: Takeaways and To Value Object or Not To: How I choose Value Objects.

5 Unit Testing Best Practices I Learned from This NDC Conference Talk

Recently, I found a NDC talk titled “.NET Testing Best Practices” by Rob Richardson.

Today I want to share five unit testing best practices I learned from that talk, along with my comments on other parts of it.

Here’s the YouTube video of the talk, in case you want to watch it, and the speaker’s website,

During the presentation, the speaker coded some unit tests for the LightActuator class. This class powers an IoT device that turns a light switch on or off based on a motion sensor input.

The LightActuator turns on lights if any motion is detected in the evening or at night. And, it turns off lights in the morning or if no motion has been detected in the last minute.

Here’s the LightActuator class, Source

public class LightActuator : ILightActuator
{
    private DateTime LastMotionTime { get; set; }

    public void ActuateLights(bool motionDetected)
    {
        DateTime time = DateTime.Now;

        // Update the time of last motion.
        if (motionDetected)
        {
            LastMotionTime = time;
        }

        // If motion was detected in the evening or at night, turn the light on.
        string timePeriod = GetTimePeriod(time);
        if (motionDetected && (timePeriod == "Evening" || timePeriod == "Night"))
        {
            LightSwitcher.Instance.TurnOn();
        }
        // If no motion is detected for one minute, or if it is morning or day, turn the light off.
        else if (time.Subtract(LastMotionTime) > TimeSpan.FromMinutes(1)
                    || (timePeriod == "Morning" || timePeriod == "Noon"))
        {
            LightSwitcher.Instance.TurnOff();
        }
    }

    private string GetTimePeriod(DateTime dateTime)
    {
        if (dateTime.Hour >= 0 && dateTime.Hour < 6)
        {
            return "Night";
        }
        if (dateTime.Hour >= 6 && dateTime.Hour < 12)
        {
            return "Morning";
        }
        if (dateTime.Hour >= 12 && dateTime.Hour < 18)
        {
            return "Afternoon";
        }
        return "Evening";
    }
}

And here’s the first unit test the presenter live-coded,

public class LightActuator_ActuateLights_Tests
{
    [Fact]
    public void MotionDetected_LastMotionTimeChanged()
    {
        // Arrange
        bool motionDetected = true;
        DateTime startTime = new DateTime(2000, 1, 1); // random value

        // Act
        LightActuator actuator = new LightActuator();
        actuator.LastMotionTime = startTime;
        actuator.ActuateLights(motionDetected);
        DateTime actualTime = actuator.LastMotionTime;

        // Assert
        Assert.NotEqual(actualTime, startTime);
    }
}

Of course, the presenter refactored this test and introduced more examples throughout the rest of the talk. But this initial test is enough to prove our points.

Five Unit Testing Best Practices from This Talk

1. Adopt a new naming convention

In this talk, I found a new naming convention for our unit tests.

To name test classes, we use <ClassName>_<MethodName>_Tests.

For test methods, we use <Scenario>_<ExpectedResult>.

Here are the test class and method names for our sample test,

public class LightActuator_ActuateLights_Tests
//           ^^^^^
{
    [Fact]
    public void MotionDetected_LastMotionTimeChanged()
    //          ^^^^^
    {
        // Beep, beep, boop...
        // Magic goes here
    }
}

2. Label your test parameters

Instead of simply calling the method under test with a list of parameters, let’s label them for more clarity.

For example, instead of simply calling ActuateLights() with true, let’s create a documenting variable,

[Fact]
public void MotionDetected_LastMotionTimeChanged()
{
    bool motionDetected = true;
    DateTime startTime = new DateTime(2000, 1, 1);

    LightActuator actuator = new LightActuator();
    actuator.LastMotionTime = startTime;
    // Before
    //actuator.ActuateLights(true);
    //                       ^^^^^
    // After
    actuator.ActuateLights(motionDetected);
    //                     ^^^^^
    DateTime actualTime = actuator.LastMotionTime;

    // Beep, beep, boop...
}

3. Use human-friendly assertions

Looking closely at the sample test, we notice the Assert part has a bug.

The actual and expected values inside NotEqual() are in the wrong order. The expected value should go first. Arrrggg!

[Fact]
public void MotionDetected_LastMotionTimeChanged()
{
    bool motionDetected = true;
    DateTime startTime = new DateTime(2000, 1, 1);

    LightActuator actuator = new LightActuator();
    actuator.LastMotionTime = startTime;
    actuator.ActuateLights(motionDetected);
    DateTime actualTime = actuator.LastMotionTime;

    Assert.NotEqual(actualTime, startTime);
    //              ^^^^^
    // They're in the wrong order. Arrrggg!
}

To avoid flipping them again, it’s a good idea to use more human-friendly assertions using libraries like FluentAssertions or Shouldly.

Here’s our tests using FluentAssertions,

[Fact]
public void MotionDetected_LastMotionTimeChanged()
{
    bool motionDetected = true;
    DateTime startTime = new DateTime(2000, 1, 1);

    LightActuator actuator = new LightActuator();
    actuator.LastMotionTime = startTime;
    actuator.ActuateLights(motionDetected);
    DateTime actualTime = actuator.LastMotionTime;

    // Before
    //Assert.NotEqual(actualTime, startTime);
    //                ^^^^^
    // They're in the wrong order. Arrrggg!
    //
    // After, with FluentAssertions
    actualTime.Should().NotBe(startTime);
    //         ^^^^^
}

4. Don’t be too DRY

Our sample test only covers the scenario when any motion is detected. If we write another test for the scenario with no motion detected, our tests look like this,

public class LightActuator_ActuateLights_Tests
{
    [Fact]
    public void MotionDetected_LastMotionTimeChanged()
    {
        bool motionDetected = true;
        //                    ^^^^
        DateTime startTime = new DateTime(2000, 1, 1);

        LightActuator actuator = new LightActuator();
        actuator.LastMotionTime = startTime;
        actuator.ActuateLights(motionDetected);
        DateTime actualTime = actuator.LastMotionTime;

        Assert.NotEqual(startTime, actualTime);
        //     ^^^^^
    }

    [Fact]
    public void NoMotionDetected_LastMotionTimeIsNotChanged()
    {
        bool motionDetected = false;
        //                    ^^^^^
        DateTime startTime = new DateTime(2000, 1, 1);

        LightActuator actuator = new LightActuator();
        actuator.LastMotionTime = startTime;
        actuator.ActuateLights(motionDetected);
        DateTime actualTime = actuator.LastMotionTime;

        Assert.Equal(startTime, actualTime);
        //     ^^^^^
    }
}

The only difference between the two is the value of motionDetected and the assertion method at the end.

We might be tempted to remove that duplication, using parameterized tests.

But, inside unit tests, being explicit is better than being DRY.

Turning our two tests into a parameterized test would make us write a weird Assert part to switch between Equal() and NotEqual() based on the value of motionDetected.

Let’s prefer clarity over dryness. Tests serve as a living documentation of system behavior.

5. Replace dependency creation with auto-mocking

ActuateLights() uses a static class to turn on/off lights,

public void ActuateLights(bool motionDetected)
{
    DateTime time = DateTime.Now;

    if (motionDetected)
    {
        LastMotionTime = time;
    }

    string timePeriod = GetTimePeriod(time);
    if (motionDetected && (timePeriod == "Evening" || timePeriod == "Night"))
    {
        LightSwitcher.Instance.TurnOn();
        // ^^^^^
    }
    else if (time.Subtract(LastMotionTime) > TimeSpan.FromMinutes(1)
                || (timePeriod == "Morning" || timePeriod == "Noon"))
    {
        LightSwitcher.Instance.TurnOff();
        // ^^^^^
    }
}

It’d be hard to assert if the lights were turned on or off with a static method.

A better approach is to replace LightSwitcher.Instance with an interface.

But adding a new dependency to the LightActuator would break our tests.

Instead of manually passing the new LightSwitch abstraction to the LightActuator constructor inside our tests, we could rely on auto-mocking tools like Moq.AutoMocker.

Here’s our test using AutoMocker,

[Fact]
public void MotionDetected_LastMotionTimeChanged()
{
    bool motionDetected = true;
    DateTime startTime = new DateTime(2000, 1, 1);

    var ioc = new AutoMocker();
    //            ^^^^^
    var actuator = ioc.CreateInstance<LightActuator>();
    //                 ^^^^^

    actuator.LastMotionTime = startTime;
    actuator.ActuateLights(motionDetected);
    DateTime actual = actuator.LastMotionTime;

    actual.Should().NotBe(startTime);
}

I’ve already used a similar approach with TypeBuilder and AutoFixture.

My Reaction: What I’d do differently

After writing and getting my tests reviewed, I’ve developed my own “taste” for unit testing.

Don’t take me wrong. This is a good talk and I’ve stolen some ideas for my own presentations.

But, this is what I’d do differently:

1. Avoid comments for AAA sections

Let’s avoid adding // Arrange, // Act, and // Assert comments inside our tests.

We don’t add // class, // fields, and // methods in other parts of our code, so it shouldn’t be necessary in our tests either.

Instead, I prefer using blank lines to visually separate the three sections of the Arrange/Act/Assert pattern.

In the examples I’ve shown you, I completely removed those comments.

2. Name test values instead of using comments

It’s a good idea to document our test values. But, let’s avoid using comments when we can use a descriptive name.

I’d rename startTime with a comment at the end to anyStartTime or randomStartTime,

[Fact]
public void MotionDetected_LastMotionTimeChanged()
{
    bool motionDetected = true;
    // Before:
    //DateTime startTime = new DateTime(2000, 1, 1); // random value
    //                                               ^^^^^
    var anyStartTime = new DateTime(2000, 1, 1);
    //  ^^^^^
    // or
    //var randomStartTime = new DateTime(2000, 1, 1);
    //    ^^^^

    LightActuator actuator = new LightActuator();
    actuator.LastMotionTime = anyStartTime;
    actuator.ActuateLights(motionDetected);
    DateTime actualTime = actuator.LastMotionTime;

    Assert.NotEqual(anyStartTime, actualTime);
}

3. Don’t expose private parts

In the talk, as part of the refactoring session, the presenter tested some internals. Specifically, he made the LastMotionTime property inside the LightActuator class public to use it inside the tests.

Even somebody in the audience raised this question too.

I understand the presenter had less than an hour to show a complete example and he chose a simple approach.

But, let’s avoid exposing internals to our tests. That’s the most common mistake on unit testing.

Parting thoughts

Voilà! Those are the five lessons I learned from this talk.

My favorite quote from the talk:

“What’s cool about unit testing is we can debug our code by writing code”

— Rob Richardson

As an exercise left to the reader, the presenter didn’t cover testing time. But we already covered how to write tests that use DateTime.Now using a custom abstraction.

And if we’re using .NET 8.0, we can rely on .NET 8.0 TimeProvider and its abstractions to abstract and test time.

Another thing I didn’t like is that at some point in the testing session, a TimePeriodHelper was added. And that’s one of the method and class names I’d like to ban.

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.

What I Don't Like About C# Evolution: Inconsistency

C# isn’t just Java anymore.

That might have been true for the early days of C#. But the two languages took different paths.

People making that joke have missed at least the last ten years of C# history.

C# is open source and in constant evolution. In fact, you can upvote and discuss feature proposals in the C# official GitHub repo.

Every .NET release comes with new C# features.

But I’ve stopped obsessing over new C# features. In fact, I stopped collecting my favorite C# features by version. I feel I’m not missing much.

C# is not a consistent language anymore.

We now have many alternatives for the same task. Don’t believe me?

Objects, Arrays, and Null

First, here’s how to create a new object,

Movie m = new Movie("Titanic");
var m = new Movie("Titanic");
Movie m = new("Titanic");
//        ^^^^

The last one is the target-typed “new” expressions introduced in C# 9.0. That’s one of the features I disable in a .editorconfig file.

Second, here’s how to declare and initialize an array,

Movie[] movies = new Movie[] { new Movie("Titanic") };

var movies = new Movie[] { new Movie("Titanic") };
var movies = new Movie[] { new("Titanic") };
//                         ^^^^^

var movies = new[] { new Movie("Titanic") };
//           ^^^^^

Movie[] movies = [ new Movie("Titanic") ];
Movie[] movies = [ new("Titanic") ];
//              ^^^

We combine “new” expressions and collection expressions introduced in C# 12.

And, lastly, here’s how to check if an object is not null,

var titanic = new Movie("Titanic")

titanic != null;
titanic is not null;
titanic is {};
titanic is object;

And I don’t want to touch on primary constructors. It’s like classes got jealous of records and started crying for a similar feature, like a baby boy jealous of his brother.

Voilà! That’s what I don’t like about C#. Don’t take me wrong, C# is a great language with excellent tooling. But my favorite features are quite old: LINQ, async/await, and extension methods.

Some new features have lowered the barrier to entry. Now a “Hello, world” is a single line of code: Console.WriteLine("Hello, world!");.

Other C# features are making the language inconsistent and easier to write but not easier to read.

Often C# doesn’t feel like a single language, but three: the classic one from the early 2000s, another one between the classic and the .NET era, and the one we’re living these days.

Anyway, still no discriminated unions. Maybe in C# 20?

For more C# content, read my C# Definitive Guide, how to avoid exceptions when working with Dictionaries, and Six helpful extension methods to work with Collections.

Happy coding!

New Developers Looking for a Mentor: Here's a (Free) Mentorship Session in 8 Lessons

You don’t need to meet your mentors.

I read that Napoleon Hill, the author of “Think and Grow Rich,” talked to his mentor only once, and he said it changed his life.

I’d be happy to share some advice over a virtual coffee in an “ask-a-friend” style. But to preserve my keystrokes and help more than one person, here I go.

I’m writing for my 20-year-old self joining his first corporate job, pretending that talent was a shortcut to break the rules of the corporate world. I thought my first job would be like in a Silicon Valley startup, working barefoot and sliding between offices. It was far from that.

This is free Internet advice, so handle it with care.

If you could take away only one lesson, take this first one,

1. Learn It and Teach It

“The moment you learn something, teach it.”

That’s from the book Show Your Work by Austin Kleon. “A book for people who hate the very idea of self-promotion.”

Teaching is the most effective way to learn. When you teach what you’ve learned, you consolidate your learning and, if you choose to teach online, you start building an online brand.

If you’re still here for more lessons, let’s continue…

2. Google “How To Be a Good Developer”

I remember doing this exact same search, back in the early 2010s when I started my journey.

I remember finding “write technical specs” and “learn functional programming.” It gave me ideas and subjects to explore. That’s why I’ve always had Functional Programming in the back of my head.

I’d tell my younger self to do it again.

Google or DuckDuckGo or Bing “How to be a good developer” and see how deep the rabbit hole goes.

Be careful, you will find one post saying “Document your code” and another one, saying “Don’t document your code.” Separate the wheat from the shaft.

3. Practice a Lot

The barrier to entry into the coding world is low, and lower and lower with every day that passes.

Anyone can potentially start, but getting good at it takes years.

Another googling task: search for “Teach Yourself Programming in Ten Years.”

Start by writing an end-to-end project: a coding project that reads data from a webpage, calls a backend, persists data into a relational database, and displays it back.

You will learn a lot from this simple exercise. HTML/CSS, a UI library, HTTP/REST, a backend language, SQL, and a database engine. Quite a lot!

I can’t remember how many hours I spent coding a recipe catalog back in the day with PHP and MySQL.

4. Master the Fundamentals

Frameworks and libraries come and go.

Study subjects that stand the test of time. Design patterns, data structures, and SQL, for example.

For some reason, we’re still talking about stoicism thousands of years later.

Probably in the next decade, we will still be writing code on text files, using some flavor of Unix/Linux, and writing SQL. I wouldn’t bet all my money, though.

5. Invest in Your Soft Skills

You won’t be locked in your basement coding. This is a collaborative endeavor.

You will spend most of your time communicating and collaborating: 1-1s, estimations, and SCRUM “ceremonies.”

Invest in your soft skills. Start by reading “How to Win Friends and Influence People.” My biggest takeaway from that book is never telling someone is wrong.

6. Read the Clean Code

This is the book we all start with.

It opened a whole new world for me. Nobody taught me about good variable and function names until I found that book. I even remember one of my teachers using “stu1” and “stu2” as variable names during classes. Uncle Bob would be pissed by those names.

Don’t just read it. Study it.

But don’t become a Clean Code police officer issuing infractions around you and using your book copy as your baton. There’s always a tradeoff.

7. Ask Yourself What You Want

Start by experimenting with roles and tech stacks. You will find the one that you like the most.

I started writing PDF reports by hand, drawing lines and cells, one pixel and line at a time. I don’t do that anymore, by the way. Then, I did a bit of mobile development with Xamarin and frontend development back in the day of Bootstrap and Knockout.js. Eventually, I got tired of styling and coloring issues and moved to backend development.

Ask yourself what you want out of your career. Is it money? Connections? Growing your own business? Write a 5-year plan and be willing to correct the course. “No plan resists contact with the enemy” or reality, I prefer to say.

8. Climbing the Corporate Ladder Is a Myth

You don’t control anything about the ladder you climb. Anyone can add a new step or replace the entire ladder, at any time.

Software Engineer I, II, III, IV, V…

You will hit a point of diminishing returns and a glass ceiling as a software developer.

I’d tell my younger self eager for a fancy title: a new title comes with more meetings, extra hours, and the same salary.

Instead of optimizing for a title, optimize for a lifestyle. The best ladder to climb is the one you build for yourself.

Takeaways

Voila! That’s what I’d tell my younger self. I hope I haven’t discouraged you. Software Engineering is a great career. It’s the future. Well, that’s what I’ve been hearing for quite a while.

The most satisfying thing about this career is seeing a smile on your users’ faces when what you code helps them save hours of repetitive work.

For more practical advice…

Refactor your coding career with my free 7-day email course. In just 7 emails, you’ll get 10+ years of career lessons to avoid costly career mistakes.

See you around and happy coding!

Testing DateTime.Now Revisited: .NET 8.0 TimeProvider

Starting from .NET 8.0, we have new abstractions for time. We don’t need a custom ISystemClock interface. There’s one built-in. Let’s learn how to use the new TimeProvider class to write tests that use DateTime.Now.

.NET 8.0 added the TimeProvider class to abstract date and time. It has a virtual method GetUtcNow() that sets the current time inside tests. It also has a non-testable implementation for production code.

Let’s play with the TimeProvider by revisiting how to write tests that use DateTime.Now.

Back in the day, we wrote two tests to validate expired credit cards. And we wrote an ISystemClock interface to control time inside our tests. These are the tests we wrote:

using FluentValidation;
using FluentValidation.TestHelper;

namespace TimeProviderTests;

[TestClass]
public class CreditCardValidationTests
{
    [TestMethod]
    public void CreditCard_ExpiredYear_ReturnsInvalid()
    {
        var when = new DateTime(2021, 01, 01);
        var clock = new FixedDateClock(when);
        var validator = new CreditCardValidator(clock);
        //                                      ^^^^^
        // Look, ma! I'm going back in time

        var creditCard = new CreditCardBuilder()
                        .WithExpirationYear(DateTime.UtcNow.AddYears(-1).Year)
                        .Build();
        var result = validator.TestValidate(creditCard);

        result.ShouldHaveAnyValidationError();
    }

    [TestMethod]
    public void CreditCard_ExpiredMonth_ReturnsInvalid()
    {
        var when = new DateTime(2021, 01, 01);
        var clock = new FixedDateClock(when);
        var validator = new CreditCardValidator(clock);
        //                                      ^^^^^
        // Look, ma! I'm going back in time again

        var creditCard = new CreditCardBuilder()
                        .WithExpirationMonth(DateTime.UtcNow.AddMonths(-1).Month)
                        .Build();
        var result = validator.TestValidate(creditCard);

        result.ShouldHaveAnyValidationError();
    }
}

public interface ISystemClock
{
    DateTime Now { get; }
}

public class FixedDateClock : ISystemClock
{
    private readonly DateTime _when;

    public FixedDateClock(DateTime when)
    {
        _when = when;
    }

    public DateTime Now
        => _when;
}

public class CreditCardValidator : AbstractValidator<CreditCard>
{
    public CreditCardValidator(ISystemClock systemClock)
    {
        var now = systemClock.Now;
        // Beep, beep, boop
        // Using now to validate credit card expiration year and month...
    }
}

We wrote a FixedDateClock that extended ISystemClock to freeze time inside our tests. The thing is, we don’t need them with .NET 8.0.

1. Use TimeProvider instead of ISystemClock

Let’s get rid of our old ISystemClock by making our CreditCardValidator receive TimeProvider instead, like this:

public class CreditCardValidator : AbstractValidator<CreditCard>
{
    // Before:
    // public CreditCardValidator(ISystemClock systemClock)
    // After:
    public CreditCardValidator(TimeProvider systemClock)
    //                         ^^^^^
    {
        var now = systemClock.GetUtcNow();
        // or
        //var now = systemClock.GetLocalNow();
        
        // Beep, beep, boop
        // Rest of the code here...
    }
}

The TimeProvider abstract class has the GetUtcNow() method to override the current UTC date and time. Also, it has the LocalTimeZone property to override the local timezone. With this timezone, GetLocalNow() returns the “frozen” UTC time as a local time.

If we’re working with Task, we can use the Delay() method to create a task that completes after, well, a delay. Let’s use the short delays in our tests to avoid making our tests slow. Nobody wants a slow test suite.

With the TimeProvider, we can control time inside our tests by injecting a fake. But for production code, let’s use TimeProvider.System. It uses DateTimeOffset.UtcNow under the hood.

person holding glass ball
Time from another perspective. Photo by Jossuha Théophile on Unsplash

2. Use FakeTimeProvider instead of FixedDateClock

We might be tempted to wrie a child class that extends TimeProvider. But, let’s hold our horses. There’s an option for that too.

Let’s rewrite our tests after that change in the signature of the CreditCardValidator.

First, let’s install the Microsoft.Extensions.TimeProvider.Testing NuGet package. It has a fake implementation of the time provider: FakeTimeProvider.

Here are our two tests using the FakeTimeProvider:

using FluentValidation;
using FluentValidation.TestHelper;
using Microsoft.Extensions.Time.Testing;

namespace TestingTimeProvider;

[TestClass]
public class CreditCardValidationTests
{
    [TestMethod]
    public void CreditCard_ExpiredYear_ReturnsInvalid()
    {
        // Before:
        //var when = new DateTime(2021, 01, 01);
        //var clock = new FixedDateClock(when);
        var when = new DateTimeOffset(2021, 01, 01, 0, 0, 0, TimeSpan.Zero);
        var clock = new FakeTimeProvider(when);
        //              ^^^^^
        // Look, ma! No more ISystemClock
        var validator = new CreditCardValidator(clock);
        //                                      ^^^^^

        var creditCard = new CreditCardBuilder()
                        .WithExpirationYear(DateTime.UtcNow.AddYears(-1).Year)
                        .Build();
        var result = validator.TestValidate(creditCard);

        result.ShouldHaveAnyValidationError();
    }

    [TestMethod]
    public void CreditCard_ExpiredMonth_ReturnsInvalid()
    {
        // Before:
        //var when = new DateTime(2021, 01, 01);
        //var clock = new FixedDateClock(when);
        var when = new DateTimeOffset(2021, 01, 01, 0, 0, 0, TimeSpan.Zero);
        var clock = new FakeTimeProvider(when);
        //              ^^^^^
        var validator = new CreditCardValidator(clock);
        //                                      ^^^^^
        // Look, ma! I'm going back in time

        var creditCard = new CreditCardBuilder()
                        .WithExpirationMonth(DateTime.UtcNow.AddMonths(-1).Month)
                        .Build();
        var result = validator.TestValidate(creditCard);

        result.ShouldHaveAnyValidationError();
    }
}

The FakeTimeProvider has two constructors. One without parameters sets the internal date and time to January 1st, 2000, at midnight. And another one that receives a DateTimeOffset. That was the one we used in our two tests.

The FakeTimeProvider has two helpful methods to change the internal date and time: SetUtcNow() and Advance(). SetUtcNow() receives a new DateTimeOffset and Advance(), a TimeSpan to add it to the internal date and time.

If we’re curious, this is the source code of TimeProvider and FakeTimeProvider from the official dotnet repository on GitHub.

If we take a closer look at our tests, we’re “controlling” the time inside the CreditCardValidator. But, we still have DateTime.UtcNow when creating a credit card. For that, we can introduce a class-level constant Now. But that’s an “exercise left to the reader.”

Voilà! That’s how to use the new .NET 8.0 abstraction to test time. We have the new TimeProvider and FakeTimeProvider. We don’t need our ISystemClock and FixedDateClock anymore.

If you want to read more content, check how to Test Logging Messages with FakeLogger and my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to other 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.

Happy testing!