Welcome to my world!

Welcome to my world!

I'm a software engineer who cares deeply about clarity, usability, and getting the details right.

I'm a software engineer who cares deeply about clarity, usability, and getting the details right.

Brand Logo
Icon
1

<!--

Work info

-->

Company:

VAE, Inc.

Role:

Software Engineer

Year:

2025

Work Image

Project Overview

This project focused on upgrading a production PostgreSQL integration from an older Npgsql version to Npgsql 8.0, while preserving correctness, improving type safety, and maintaining compatibility with existing infrastructure such as source-generated database commands.

Although the upgrade appeared straightforward on the surface, Npgsql 8 introduced several breaking changes that surfaced long-standing assumptions in the codebase—particularly around null handling, parameter binding, and PostgreSQL type mapping. My role was to work through these changes directly in the production codebase, refactor legacy patterns, and ensure the upgrade unblocked future work rather than introducing new technical debt.

Background: Why the Upgrade Mattered

Npgsql 8.0 introduced meaningful changes intended to improve:

  • nullability correctness

  • type safety

  • performance characteristics

  • compatibility with NativeAOT and trimming scenarios

These changes aligned well with the broader architectural direction of the system, particularly the move toward compile-time guarantees via source generators. However, they also made previously “working” patterns invalid or discouraged.

The upgrade therefore wasn’t just a dependency bump—it required revisiting how database interaction was modeled throughout the application.

Problem Statement

The primary challenge of the Npgsql 8 upgrade was that legacy patterns compiled and ran, but were no longer compatible with the stricter expectations introduced in the new version.

Specifically:

  • Use of DBNull obscured nullability intent and conflicted with generator compatibility

  • Parameter binding relied on loosely typed APIs that masked mismatches

  • PostgreSQL types were sometimes treated generically rather than explicitly

  • New Npgsql features required opt-in due to incompatibility with NativeAOT and trimming

Simply updating the package surfaced compile-time errors, runtime warnings, and subtle behavioral changes that needed to be addressed deliberately.

Approach & Constraints

Several constraints shaped how the upgrade could be done:

  • The upgrade had to preserve existing behavior while improving correctness

  • DBNull could not be used, as it blocked source-generated command support

  • Nullability needed to be explicit and consistent across SQL, parameters, and models

  • PostgreSQL-specific types needed to be modeled accurately

  • The system needed to remain compatible with future NativeAOT and trimming scenarios

  • Changes had to be incremental and verifiable

Rather than patching over failures, I treated the upgrade as an opportunity to align the data-access layer with stricter, more explicit contracts.

Key Decisions
Adopt nullable generic parameters instead of DBNull

One of the most impactful changes in Npgsql 8 was improved support for nullable value types through NpgsqlParameter<T?>.

Legacy code relied heavily on DBNull to represent missing values. While this worked at runtime, it:

  • obscured intent

  • bypassed type checking

  • blocked source generation

  • made nullability harder to reason about

I refactored parameter usage to rely on nullable generics, making nullability explicit in method signatures and enabling the compiler and generator to reason about it correctly.

Model PostgreSQL types explicitly

The upgrade also pushed toward explicit modeling of PostgreSQL types rather than relying on implicit or loosely typed representations.

Examples included:

  • mapping cidr and inet to Npgsql-provided types

  • avoiding generic object or string-based representations

  • aligning C# types with database semantics rather than convenience

This reduced ambiguity and made parameter binding more predictable.

Treat upgrade errors as design feedback

Rather than suppressing warnings or opting into dynamic features prematurely, I treated upgrade friction as feedback about mismatched assumptions in the existing code.

This meant:

  • tracing errors back to model or parameter definitions

  • revisiting SQL assumptions when types did not align

  • preferring structural fixes over local workarounds

This approach prevented the upgrade from becoming a series of fragile exceptions.

Preserve compatibility with source generation and future tooling

Throughout the upgrade, I was careful to avoid changes that would block or complicate other ongoing efforts, particularly source-generated database commands.

This meant:

  • avoiding APIs incompatible with source generation

  • respecting opt-in requirements for dynamic features

  • making choices that would remain valid under NativeAOT and trimming

The goal was not just to “get to green,” but to ensure the system remained evolvable.

Tradeoffs & Risks

The upgrade introduced several tradeoffs:

  • increased upfront refactoring effort

  • stricter compile-time constraints

  • the need to revisit code that had “worked for years”

These costs were accepted because they eliminated ambiguity, reduced reliance on runtime behavior, and aligned the system with modern .NET and PostgreSQL practices.

Results and Impact

The Npgsql 8.0 upgrade resulted in a more explicit, type-safe, and future-compatible data-access layer, with concrete improvements across multiple production paths.

Key outcomes included:

  • Upgraded 20+ PostgreSQL data-access paths to be compatible with Npgsql 8.0, preserving existing behavior while removing patterns incompatible with stricter nullability and typing requirements

  • Eliminated legacy DBNull usage across migrated paths, unblocking compatibility with source-generated database commands and NativeAOT constraints

  • Refactored parameter binding and type mappings to make nullability and PostgreSQL-specific semantics explicit rather than implicit

  • Resolved multiple classes of upgrade-related failures (compile-time errors, runtime warnings, and subtle behavioral changes) by addressing root causes instead of introducing suppressions

  • Preserved and strengthened source-generator compatibility, ensuring the upgrade did not regress parallel modernization efforts in the data-access layer

  • Positioned the system to adopt future .NET and PostgreSQL features without reintroducing legacy null-handling or dynamic typing patterns

As a result, database interactions became easier to reason about, safer by default, and better aligned with the system’s long-term architectural direction.

Reflections and Takeaways

This work reinforced that dependency upgrades are often architecture reviews in disguise. Npgsql 8 forced the system to be honest about nullability, typing, and intent—areas that had previously relied on convention.

Key takeaways:

  • Version upgrades surface hidden assumptions

  • Explicit typing reduces long-term maintenance cost

  • Nullability must be modeled, not inferred

  • Compatibility with future tooling should guide present decisions

What I Intentionally Did Not Do

I avoided introducing compatibility shims or suppressions that would hide underlying issues. While these would have reduced short-term effort, they would have preserved the very ambiguity the upgrade exposed.

Instead, the focus remained on structural fixes that aligned with the system’s long-term direction.

Results

0

Data-access paths upgraded

0

Elimination of legacy DBNull usages across migrated paths

0

PostgresSQL network types explicitly modeled

Building and evolving production systems under real-world constraints

Social Icon
Social Icon

Building and evolving production systems under real-world constraints

Social Icon
Social Icon

Building and evolving production systems under real-world constraints

Social Icon
Social Icon