TIL: What Entity Framework Core Do With Nullable Foreign Keys

In another episode of Adventures with Entity Framework…

I expected Include() to always translate to an INNER JOIN. But with nullable “joining” properties, EF Core uses a LEFT JOIN.

Here’s my replicated scenario with movies and directors.

#1. Let’s create a Movies and Directors table with no foreign keys between them.

USE Movies;
GO
CREATE TABLE Movies (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL
);
GO
CREATE TABLE Directors (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    MovieId INT NULL, /* <- A nullable column here */
    /* And no constraint here. I'm a legacy app */
);
GO

#2. Let’s store (and retrieve) one movie and its director and an orphan director.

using Microsoft.EntityFrameworkCore;

namespace NullableForeignKeys;

public class Movie
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Director> Directors { get; set; }
}

public class Director
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? MovieId { get; set; }
	//     ^^^^
	// A nullable property here
    public Movie Movie { get; set; }
}

public class MovieContext : DbContext
{
    public MovieContext(DbContextOptions<MovieContext> options) : base(options)
    {
    }

    public DbSet<Movie> Movies { get; set; }
    public DbSet<Director> Directors { get; set; }
}

[TestClass]
public class MovieTests
{
    [TestMethod]
    public async Task NullableForeignKey()
    {
        const string connectionString = $"Server=(localdb)\\MSSQLLocalDB;Database=Movies;Trusted_Connection=True;";

        var options = new DbContextOptionsBuilder<MovieContext>()
            .UseSqlServer(connectionString)
            .Options;

        using (var context = new MovieContext(options))
        {
			// An orphan director
            context.Directors.Add(new Director
            {
                Name = "Quentin Tarantino"
            });
			// A movie and its director
            context.Movies.Add(new Movie
			{
				Name = "Titanic",
				Directors =
				[
					new Director
					{
						Name = "James Cameron",
					}
				]
			});
            context.SaveChanges();
        }

        using (var context = new MovieContext(options))
        {
            // Imagine a query with filters on director and movie
            var directors = await context.Directors
                    .Include(d => d.Movie)
                    // I thought this would retrieve
                    // directors with movies
                    .ToListAsync();

            foreach (var d in directors)
            {
                Assert.IsNotNull(d);
                Assert.IsNotNull(d.Movie);
                //     ^^^^^
                // This one breaks...
            }
        }
    }
}

In this legacy app, child entities could exist without parents. The real query also applied filters on both entities.

Now let’s look at the SQL EF Core generated:

SELECT [d].[Id], [d].[MovieId], [d].[Name], [m].[Id], [m].[Name]
      FROM [Directors] AS [d]
      LEFT JOIN [Movies] AS [m] ON [d].[MovieId] = [m].[Id]

Notice the LEFT JOIN. Using int Movie instead produces an INNER JOIN.

Lesson: JOIN type depends on the joining property’s nullability, not on Include() itself.

One Simple Shift That Turned My Content Into Sales

I tried the “proven” formula the big names swear by and it failed miserably.

I did what most people recommend to sell:

  1. Write social media and long-form posts.
  2. Offer a free email course or opt-in.
  3. Take people to a newsletter.
  4. Pitch your offer or products.

But after months of content, I realized something was broken.

The realization that changed everything

I packed my best career LinkedIn posts into a free 7-day email course.

I offered it as “pay what you want” and promoted it on LinkedIn. A few readers left a tip. It gave me momentum to keep promoting it.

But when I sent emails, almost nobody clicked my offers. After more than 30 emails, my products made just $1. OK, maybe my copywriting wasn’t strong or I was selling the wrong products.

The math doesn’t add up.

The change that brought the sales

Every step between your reader and your offer makes a sale harder.

It’s like walking people into your store, then sending them away for a course before selling what’s already on your shelves. By the time you follow up, they already bought somewhere else and forgot about you.

For my content strategy, I removed the intermediate steps. Social media and long-form posts -> products. Now I plug my book sales pages inside or at the end of posts.

With that simple fix, sales came in and I passed the $1 test. My first book, Street-Smart Coding, got two pre-sales within weeks of announcing it.

It wasn’t emails or “nurturing.” It was removing friction. A confused reader takes no action.

Remove the friction. Make action effortless. Let every piece of content be a sales representative.

A Short Story to Become Wiser (and Be More Present)

One day, a man approached a Zen master with a question.

“Master, what can a simple man like me do to become wiser like you?”

The Zen master said, “Well, I just sleep, eat, and talk.”

“Hmm, I already do that. But I’m not a wise man like you,” the man said.

“You may do that. But when I sleep, I’m simply sleeping. When I eat, I’m simply eating. And when I talk, I’m simply talking. When you sleep, you remember problems. When you eat, you use your phone. And when you talk, you think about what to ask next or how to answer.”

I first heard that story in a Sunday sermon, and it stayed with me.

It reminds me of a lesson from Eckhart Tolle’s The Power of Now. Our mind is like a time machine, taking us to the past (where guilt and resentment live) and into the future (where anxiety lives). But wisdom begins when we step off that machine and live in the present.

2026 Start-of-Year Stats to Keep Myself Accountable

For the first time, I’m putting my numbers out there.

Mark Thompson shared his stats on Medium. The point wasn’t to show off, but to hold ourselves accountable with our growing goals.

Here are some of my stats, starting 2026:

#1. Blog: My blog is my content hub. All my ideas start or end on my blog. Then, I syndicate in other places.

Total so far: 708 posts. Last year, I posted daily and hit the 400-daily posts milestone. For a recap, here’s my 2025 in review.

#2. Medium: This is my main driver of traffic and book sales. Last year, I saw a big bump in views and followers. So far: 1,197 followers and 643 subscribers.

#3. LinkedIn: This is my only social media channel, where I reshare bite-sized posts. So far: 832 followers and 364 connections.

#4. dev.to: I use dev.to only for coding content.

I have a “funny” follower count: 28,552. Mostly inactive accounts. When you create a new account, the welcome wizard asks you to choose tags and follow people.

More accurate stats: 236 posts, 1,921 reactions, and 549 comments.

#5. Gumroad: This is my store for courses and books. Last year: 202 sales, 3,530 views, and one new book launched.

#6. Friday Links: This is the “backend” for my product sales. Each Friday, I send 4 curated links. So far: 146 subscribers and 53 issues sent.

My plan for 2026

In 2026, I’m focusing on simplifying my content strategy, not chasing followers.

Here’s what I’m doing:

  • I’m using dev.to API to automatically repost my posts with a simple script.
  • I’m simplifying my promotion strategy for my books.
  • I’m repurposing Medium highlights on LinkedIn.

In 2026, I’m doubling down on a simple, repeatable system instead of vanity metrics.

Friday Links: The best time to be a coder

Hey, there!

Here are 4 links I thought were worth sharing this week:

#1. AI makes coding and shipping nearly free. Side projects alone won’t help you stand out (7min). Maybe it’s time to double down on open source contributions or start a “learn and build in public” YouTube channel.

#2. Addy Osmani, a leader at Google, shared 21 lessons after a decade of coding (10min). The one that resonated the most? #6. “Coding doesn’t advocate for you.” Good code alone doesn’t make you a good coder, especially in the AI era.

#3. AI shows coding isn’t the bottleneck. A concept that isn’t new. Here’s a breakdown of why it’s never been the bottleneck (6min).

#4. Maybe juniors won’t say the same, but it’s a great time to be a coder (5min). Our job may be to design systems and let AI fill in the blanks. (That’s point #10 from that post)


And in case you missed it, I wrote on my blog about the “hiring is broken” law of coding blogs (2min) and the mantra to thrive in the AI era (2min).


(Bzzz…Radio voice) This email was brought to you by… my new book, Street-Smart Coding: 30 lessons to help you code like a pro. From Googling to clear communication, it covers the lessons you don’t learn in tutorials. It’s now out on Kindle and paperback on Amazon.

Happy coding in 2026!

See you next time,

Cesar