Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions conceptual/EFCore.PG/mapping/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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<Customer>()
.ComplexProperty(c => c.Details, d => d.ToJson());
}
```

#### [Owned entities](#tab/owned entities)

```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
118 changes: 117 additions & 1 deletion conceptual/EFCore.PG/release-notes/10.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Customer>(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:
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion conceptual/EFCore.PG/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down