From 53262ddd191da6700cca669d90b351ebcab363f4 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 1 Oct 2025 15:58:39 +0200 Subject: [PATCH] Release notes and tweaks around EFCore.PG support for JSON --- conceptual/EFCore.PG/mapping/json.md | 24 ++++- conceptual/EFCore.PG/release-notes/10.0.md | 118 ++++++++++++++++++++- conceptual/EFCore.PG/toc.yml | 2 +- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/conceptual/EFCore.PG/mapping/json.md b/conceptual/EFCore.PG/mapping/json.md index 0444da08..3393eeb1 100644 --- a/conceptual/EFCore.PG/mapping/json.md +++ b/conceptual/EFCore.PG/mapping/json.md @@ -57,13 +57,13 @@ With string mapping, the EF Core provider will save and load properties to datab If your column JSON contains documents with a stable schema, you can map them to your own .NET types (or POCOs); EF will use System.Text.Json APIs under the hood to serialize instances of your types to JSON documents before sending them to the database, and to deserialize documents coming back from the database. This effectively allows mapping an arbitrary .NET type - or object graph - to a single column in the database. -EF 7.0 introduced the "JSON Columns" feature, which maps a database JSON column via EF's "owned entity" mapping concept, using `ToJson()`. In this approach, EF fully models the types within the JSON document - just like it models regular tables and columns - and uses that information to perform better queries and updates. Full support for ToJson has been added to version 8.0 of the Npgsql EF provider. +As of EF 10, the recommended way to map .NET types to JSON in the database is via complex types ([see EF release notes](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew#json)). In this mode, EF is fully aware of the structure of your JSON document - just like it's aware of your tables and columns - and provides powerful, rich querying and updating capabilities. Prior to EF 10, similar modeling was available via the "owned entity" concept, but this modeling created several issues ([see here for more details](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew#json)). If you're using EF 10 or above, complex types are the recommended way to map .NET types. -As an alternative, prior to version 8.0, the Npgsql EF provider has supported JSON POCO mapping by simply delegating serialization/deserialization to System.Text.Json; in this model, EF itself model the contents of the JSON document, and cannot take that structure into account for queries and updates. This approach can now be considered deprecated as it allows for less powerful mapping and supports less query types; using ToJson() is now the recommended way to map POCOs to JSON. +As an 3rd alternative, prior to version 8.0, the Npgsql EF provider has supported JSON POCO mapping by simply delegating serialization/deserialization to System.Text.Json; in this mode, EF itself is oblivious to the contents of the JSON document, and cannot take that structure into account for queries and updates. This approach can now be considered deprecated as it allows for less powerful mapping and supports less query types; using complex types with `ToJson()` is now the recommended way to map POCOs to JSON. -### ToJson (owned entity mapping) +### EF modeling with ToJson (recommended) -Npgsql's support for `ToJson()` is fully aligned with the general EF support; see the [EF documentation for more information](https://learn.microsoft.com/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns). +Npgsql's support for `ToJson()` is fully aligned with the general EF support; see the [EF documentation for more information](https://learn.microsoft.com/ef/core/what-is-new/ef-core-10.0/whatsnew#json). To get you started quickly, assume that we have the following Customer type, with a Details property that we want to map to a single JSON column in the database: @@ -90,6 +90,18 @@ public class Order // Part of the JSON column To instruct EF to map CustomerDetails - and within it, Order - to a JSON column, configure it as follows: +#### [Complex types (EF10+)](#tab/complex-types) + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.Entity() + .ComplexProperty(c => c.Details, d => d.ToJson()); +} +``` + +#### [Owned entities](#tab/owned entities) + ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -102,6 +114,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ``` +*** + At this point you can interact with the Customer just like you would normally, and EF will seamlessly serialize and deserialize it to a JSON column in the database. You can also perform LINQ queries which reference properties inside the JSON document, and these will get translated to SQL. ### Legacy POCO mapping (deprecated) @@ -134,7 +148,7 @@ public class Order // Part of the JSON column } ``` -### [Fluent API](#tab/fluent-api) +#### [Fluent API](#tab/fluent-api) ```csharp class MyContext : DbContext diff --git a/conceptual/EFCore.PG/release-notes/10.0.md b/conceptual/EFCore.PG/release-notes/10.0.md index 34797641..566060c6 100644 --- a/conceptual/EFCore.PG/release-notes/10.0.md +++ b/conceptual/EFCore.PG/release-notes/10.0.md @@ -2,6 +2,119 @@ Npgsql.EntityFrameworkCore.PostgreSQL version 10.0 is now in development, preview versions are available on [nuget.org](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL). +## Full support for EF 10 JSON complex types + +EF 10 introduced support for mapping .NET types as JSON complex types, resolving several issues that existed with the previous JSON mapping via owned entities ([see release notes for more information](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew#json)). The PG provider providers full support for this feature full support for this as well: + +```c# +modelBuilder.Entity(b => +{ + b.ComplexProperty(c => c.ShippingAddress, c => c.ToJson()); + b.ComplexProperty(c => c.BillingAddress, c => c.ToJson()); +}); +``` + +This configuration causes the following table to be created for your customers: + +```sql +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Name" text, + "BillingAddress" jsonb NOT NULL, + "ShippingAddress" jsonb NOT NULL, + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +``` + +This is now the preferred way to perform strongly-typed JSON mapping of arbitrary .NET types, and replaces owned entities and [legacy POCO mapping](../mapping/json.md?#legacy-poco-mapping-deprecated). + +The provider now also supports performing partial updates within JSON documents using `ExecuteUpdate`. For example, the following efficiently copies overwrites all Customers' shipping address streets with their billing address streets: + +```c# +await context.Customers.ExecuteUpdateAsync(s => + s.SetProperty(b => b.ShippingAddress.Street, b => b.BillingAddress.Street)); +``` + +This produces the following SQL: + +```sql +UPDATE "Customers" AS c +SET "ShippingAddress" = jsonb_set(c."ShippingAddress", '{Street}', c."BillingAddress" -> 'Street') +``` + +## Better support for JSON scalar (primitive) collections + +In most relational databases, scalar collections are mapped to a JSON column, the the collection is serialized to a JSON array in the database. PostgreSQL, however, is unique in providing a 1st-class array type, so the EF provider maps scalar collections to array instead. For example, given the following type: + +```c# +public class Customer +{ + public int Id { get; set; } + public string[] Tags { get; set; } +} +``` + +... the PostgreSQL provider will create the following table (note that `text[]` array column): + +```sql +CREATE TABLE "Customers" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "Tags" text[] NOT NULL, + CONSTRAINT "PK_Customers" PRIMARY KEY ("Id") +); +``` + +However, when scalar collections are nested within a JSON document, they must be mapped to JSON arrays, as in other databases: + +```c# +public class Customer +{ + public int Id { get; set; } + public Address Address { get; set; } +} + +public class Address +{ + // ... + + public string[] Tags { get; set; } +} +``` + +Version 10 of the provider now produces much better SQL when querying such nested scalar collections. For example, when querying using Contains: + +```c# +var customers = await context.Customers.Where(b => b.ShippingAddress.Tags.Contains("foo")).ToListAsync(); +``` + +... previous versions of the provider generated the following complicated (and inefficient) SQL: + +```sql +SELECT c."Id", c."Name", c."ShippingAddress" +FROM "Customers" AS c +WHERE 'foo' = ANY ((ARRAY(SELECT CAST(element AS text) FROM jsonb_array_elements_text(c."ShippingAddress" -> 'Tags') WITH ORDINALITY AS t(element) ORDER BY ordinality))) +``` + +Version 10, in contrast, produces the following cleaner SQL, which can also benefit from indexes: + +```sql +SELECT c."Id", c."Name", c."ShippingAddress" +FROM "Customers" AS c +WHERE (c."ShippingAddress" -> 'Tags') @> to_jsonb('foo'::text) +``` + +Finally, version 10 of the provider also allows you to map a non-nested scalar collection to a JSON column, instead of to an array column, and provides fully querying capabilities: + +```c# +public class Customer +{ + // ... + + [Column(TypeName = "jsonb")] + public string[] Tags { get; set; } +} +``` + ## Support for PostgreSQL 8 virtual generated columns Before PostgreSQL 18, generated (or "computed") columns could only be stored, meaning they were computed when a row is inserted or updated, and take up space on disk just like regular columns. PostgreSQL 18 introduced support for *virtual* generated columns, which are instead calculated when read, and take up no space on disk. Virtual columns can be defined with version 10 of the PostgreSQL provider as follows: @@ -19,9 +132,12 @@ Note that previously, `stored: true` had to be specified in the above code sampl For more information, [see the documentation](../modeling/generated-properties.md#computed-generated-columns). +## Support for UUIDv7 + +By default, EF generates GUID (or UUID) values locally in .NET, rather than relying on the database to generate them. Version 9 of the PG provider already switched to generating UUIDv7 values by default ([see release note](9.0.md#uuidv7-guids-are-generated-by-default)), which are significantly better for database indexes. PostgreSQL 18 also added the [`uuidv7()`](https://www.postgresql.org/docs/18/functions-uuid.html#FUNC_UUID_GEN_TABLE) built-in function, which allows database generation of UUIDv7 values. In EFCore.PG 10, if you configure the provider to target PG 18 (`.UseNpgsql("...", o => o.SetPostgresVersion(18, 0))`), the provider will also translate [`Guid.CreateVersion7()`](https://learn.microsoft.com/dotnet/api/system.guid.createversion7) to that function. + ## Other new features -* When the target PostgreSQL is version is set to 18 (`.UseNpgsql("...", o => o.SetPostgresVersion(18, 0))`), translate [`Guid.CreateVersion7()`](https://learn.microsoft.com/dotnet/api/system.guid.createversion7) to the new [`uuidv7()`](https://www.postgresql.org/docs/18/functions-uuid.html) function. * NodaTime `LocalDate.At()` and `LocalDate.AtMidnight()` are now translated. See the [10.0.0 milestone](https://github.com/npgsql/efcore.pg/milestone/68?closed=1) for the full list of Npgsql EF provider issues. diff --git a/conceptual/EFCore.PG/toc.yml b/conceptual/EFCore.PG/toc.yml index fcfc9be7..c03efd21 100644 --- a/conceptual/EFCore.PG/toc.yml +++ b/conceptual/EFCore.PG/toc.yml @@ -2,7 +2,7 @@ href: index.md - name: Release notes items: - - name: "10.0 (preview)" + - name: "10.0 (rc)" href: release-notes/10.0.md - name: "9.0" href: release-notes/9.0.md