Chapter 21 · PostgreSQL, MySQL, and Cross-Provider Portability

Identity/Sequence Strategies, Case Sensitivity, Types, JSON, Arrays, and Database-Specific Extensions

Keep a portable ServiceHub core while deliberately modeling PostgreSQL identity/sequences, arrays, jsonb and native types alongside MySQL auto-increment, charset/collation, JSON and generated-column semantics.

Advanced170–220 minutesportable-core + native-types labEF Core 10.0.11 · Npgsql EF provider 10.0.3 · Oracle MySql.EntityFrameworkCore 10.0.9 · PostgreSQL 18.6 · MySQL 8.4 LTS · .NET 10.0.11 · SDK 10.0.400Free local PostgreSQL/MySQL path · MariaDB/Pomelo EF9 observation onlyProvider compatibility reviewed: August 27, 2026

Learning outcomes

01

Separate portable domain concepts from PostgreSQL/MySQL physical type and value-generation choices.

02

Compare PostgreSQL identity/sequences/HiLo with MySQL AUTO_INCREMENT without pretending the mechanisms are interchangeable.

03

Use PostgreSQL arrays, jsonb, range and network types as intentional extensions instead of flattening them into strings for false portability.

04

Treat MySQL character set/collation, JSON and generated-column behavior as database contracts that require provider-specific migration/query tests.

05

Explain identifier case-folding, string comparison and collation differences without making universal case-sensitivity claims.

06

Inspect provider metadata and migration SQL to prove what each database will actually store.

1. The problem: one CLR model does not imply one physical data model

WorkOrder.Id is an int, Revision is a Guid, and structured ServiceHub values can be represented as JSON. Those C# facts do not say whether keys come from PostgreSQL identity/sequence or MySQL auto-increment, whether a set of tags belongs in an array or join table, how case comparison works, or which JSON operators/indexes exist. Portability starts by naming the portable requirement and then choosing each engine’s physical realization.

Avoid lowest-common-denominator modeling

Turning every native type into string makes the model superficially portable while throwing away constraints, operators and indexes. Keep a portable core where semantics truly match; isolate database-specific capabilities where they add value and test the adapter.

2. Value generation: similar goal, different mechanisms

PostgreSQL 10+ has SQL-standard identity columns backed by sequences, and Npgsql uses “identity by default” as its normal generated-int strategy. It also exposes explicit sequences and HiLo. MySQL uses AUTO_INCREMENT for the familiar generated integer key path. These choices affect migrations, seed values, bulk/import workflows and cross-provider key assumptions.

csharp · PostgreSQL-specific key strategies
modelBuilder.UseIdentityByDefaultColumns();modelBuilder.Entity<WorkOrder>()    .Property(x => x.Id)    .UseIdentityAlwaysColumn();// Advanced alternative only when measured/justified:modelBuilder.Entity<ImportBatch>()    .Property(x => x.Id)    .UseHiLo("servicehub_hilo");
csharp · portable intent, provider-specific SQL
// Portable model intent:builder.Property(x => x.Id).ValueGeneratedOnAdd();// PostgreSQL DDL can become:// integer GENERATED BY DEFAULT AS IDENTITY// MySQL DDL can become:// int NOT NULL AUTO_INCREMENT

Do not write application logic that assumes “the next integer” can be fetched the same way on every engine. EF abstracts value generation for ordinary inserts, but schema/admin/import semantics still leak.

3. Identifier and string case behavior are different questions

PostgreSQL folds unquoted identifiers to lowercase; Npgsql normally quotes identifiers generated from mixed-case CLR names. MySQL identifier case behavior can also vary by operating system/server settings. Separately, string equality/order depends on database/column collation. Therefore “PostgreSQL is case-sensitive, MySQL is case-insensitive” is too crude to be a portability rule.

Question PostgreSQL concern MySQL concern
Identifier lookup unquoted names fold to lower case; quoted names preserve case table-name case can depend on platform/server settings
String equality/order database/column collation controls semantics character set + collation controls semantics; many defaults are case-insensitive
Portable test assert business comparison semantics assert the same semantics under chosen collation
csharp · make the business comparison explicit at schema level
// Portable application rule: customer code comparison must be deterministic.// Configure provider-specific column/database collation in provider model code,// then test equality, ordering and uniqueness on each engine.var q = db.WorkOrders    .Where(w => w.CustomerName == input)    .OrderBy(w => w.CustomerName);

The test should verify rows returned and the index/plan behavior. Do not fix a collation mismatch with ToLower() everywhere; that can change index use and language semantics.

4. PostgreSQL arrays are a real relational-provider extension

Npgsql maps CLR arrays and List<T> to PostgreSQL array columns and translates many operations. For a provider-specific search profile, an array can be more honest than encoding tags into a delimited string. It is not portable to MySQL as an equivalent native array column.

csharp · PostgreSQL-only extension entity
public sealed class PgSearchProfile{    public int WorkOrderId { get; private set; }    public string[] SearchTokens { get; private set; } = [];}protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity<PgSearchProfile>()        .Property(x => x.SearchTokens)        .HasColumnType("text[]");}
csharp · provider-specific query stays in adapter
var matches = await pg.Set<PgSearchProfile>()    .Where(x => x.SearchTokens.Contains(token))    .Select(x => x.WorkOrderId)    .ToListAsync(ct);

MySQL portability choices include a normalized child table or JSON array plus provider-specific JSON predicates. Pick based on query/update/index needs, not on a desire for identical DDL.

5. jsonb, JSON, and EF Core 10 structured values are not one universal feature

PostgreSQL has jsonb with rich operators and indexes; Npgsql 10 fully supports EF Core 10 JSON complex types. MySQL has a native JSON type and its own JSON functions/indexing strategies. The portable semantic question is “what structured value do we store and query?”; the physical query/index path is provider-specific.

csharp · Npgsql EF Core 10 JSON complex mapping
public sealed record ProviderAddress(    string Line1,    string City,    string Region,    string PostalCode);public sealed class ProviderProfile{    public int Id { get; set; }    public ProviderAddress Address { get; set; } = new("", "", "", "");    public string ProviderMetadataJson { get; set; } = "{}";}modelBuilder.Entity<ProviderProfile>(b =>{    b.ComplexProperty(x => x.Address, json => json.ToJson());});// Npgsql maps the JSON complex value to jsonb in its EF10 support.
csharp · MySQL provider boundary - verify generated JSON DDL/functions
// Keep the same provider-lab entity, but put MySQL JSON configuration// in the MySQL provider project.modelBuilder.Entity<ProviderProfile>(b =>{    b.Property(x => x.ProviderMetadataJson)        .HasColumnType("json");});// Then inspect migration SQL and ToQueryString before depending on// a specific JSON member translation or generated-column index.
Compatibility discipline

Do not assume Npgsql ToJson() behavior, PostgreSQL jsonb operators, or an Npgsql GIN index API exists in Oracle MySQL. Validate each provider’s current mapping/query/index support independently.

6. PostgreSQL range/network/native types can be worth a deliberate portability seam

Npgsql directly maps PostgreSQL types such as ranges and network addresses. A ServiceHub maintenance window is naturally a tstzrange; an allow-list network can be inet. Flattening these to strings sacrifices server validation and operators. The portability boundary should expose the business capability, not the physical type.

csharp · PostgreSQL-only maintenance window
public sealed class PgMaintenanceWindow{    public long Id { get; private set; }    public NpgsqlRange<DateTime> WindowUtc { get; private set; }    public IPAddress? SourceNetwork { get; private set; }}

A MySQL implementation could use start_utc/end_utc columns plus a check constraint, and a textual/binary network representation. Contract tests should assert “contains instant” and overlap semantics—not insist both providers store the same bytes.

7. MySQL/MariaDB charset, collation, JSON, and generated columns deserve their own migration evidence

Oracle’s MySQL provider can configure character sets/collations through code-first model configuration, and MySQL supports generated columns and JSON. MariaDB has overlapping but not identical syntax/features. This is another reason a MySQL-compatible label is not enough to share an unreviewed migration stream.

sql · inspect, do not assume, MySQL server text semantics
SELECT @@character_set_server,       @@collation_server,       VERSION();SHOW CREATE TABLE work_orders;

When a generated column is introduced to index a JSON-derived value, capture the exact provider migration DDL and run it on the target MySQL release. Do not copy the same DDL into MariaDB because product names sound compatible.

8. Failure case: “portable” strings erase useful database semantics

A team stores PostgreSQL arrays, ranges, network addresses, and JSON all as arbitrary strings to keep one provider-neutral entity. Validation moves to application code, malformed values bypass constraints, search predicates stop using native operators/indexes, and the “portable” code becomes slower and more complex.

Repair: keep the core aggregate portable where its semantics are genuinely shared, then add provider-specific read/search/storage models behind adapters. The adapter is allowed to use text[], jsonb, range or MySQL generated columns when the supported product intentionally depends on those capabilities.

9. Mandatory lab: build one portable core plus two native extension examples

  1. Apply the same minimal WorkOrder model to PostgreSQL 18.6 and MySQL 8.4 LTS; capture DDL for generated IDs, Guid, text, timestamps and JSON metadata.
  2. Insert identical domain rows and verify generated-key propagation on both providers.
  3. Choose an explicit case/collation business rule for CustomerName; configure and test equality/order/uniqueness on both engines.
  4. Add a PostgreSQL-only text[] search-token projection and prove one translated containment query plus its plan.
  5. Add one MySQL-only generated/JSON-search projection or provider metadata table; capture exact DDL and EXPLAIN.
  6. Document how the alternate provider would implement the same business capability without demanding identical physical types.
  7. Drop the provider-specific lab objects/databases.

10. Production judgment and bridge

Portability is semantic, not textual. Shared EF entities are useful where both databases support the same invariants; provider-native types are valuable where they buy correctness or performance. Keep native choices in named provider modules, review their migrations, and test the business contract on every supported engine. Lesson 3 now applies the same principle to LINQ translation and generated SQL.

Check your understanding

  1. What is Npgsql’s default generated integer strategy on modern PostgreSQL?
  2. Why is “case sensitivity” not one database-wide Boolean?
  3. Should PostgreSQL arrays be replaced with comma-separated strings for portability?
  4. What is the portable contract around JSON?
  5. Why inspect MySQL SHOW CREATE TABLE after migration?
  6. What should an adapter expose for a PostgreSQL range feature?
Review the answers

1. Identity by default, with explicit identity-always, sequence and HiLo alternatives available.

2. Identifier folding and string comparison are different concerns, and string semantics depend on configured collations/character sets.

3. No. Use a provider-specific capability when it adds value, and provide a different implementation such as a child table or JSON for other providers.

4. The domain shape and required query/update semantics; storage type, operators, indexes and translation are provider-specific.

5. It proves the actual character set, collation, generated columns, indexes and provider-specific DDL the database accepted.

6. The business operation such as contains/overlaps, not the requirement that every provider expose the Npgsql range CLR type.

Authoritative references

Use provider and database documentation together when a model depends on native types or value generation.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n