Skip to content

Latest commit

 

History

History
248 lines (179 loc) · 11.8 KB

File metadata and controls

248 lines (179 loc) · 11.8 KB

8.0 Release Notes

Npgsql.EntityFrameworkCore.PostgreSQL version 8.0 is out and available on nuget.org.

Full support for primitive collection querying

One of PostgreSQL unique features as a relational database is its rich support for array types, which can be stored just like any other basic data type and queried. The Npgsql EF provider has supported mapping .NET arrays and Lists to PostgreSQL arrays for a very long time; however, with the introducion of rich primitive collection support in EF itself, Npgsql's support for arrays was extended to include full querying capabilities.

Column collections

On to the code! You can now use any LINQ operator - or chain of operators - on top of a primitive array or list property. For example, the following queries for all blogs who have at least 3 tags starting with "x":

class Blog
{
    ...
    public List<string> Tags { get; set; }
}

var blogs = await ctx.Blogs
    .Where(b => b.Tags.Count(t => t.StartsWith("x")) > 3)
    .ToListAsync();

This is now translated to the following SQL:

SELECT b."Id", b."Tags"
FROM "Blogs" AS b
WHERE (
    SELECT count(*)::int
    FROM unnest(b."Tags") AS t(value)
    WHERE t.value LIKE 'x%') > 3

Such complex translations rely on the PostgreSQL unnest function, which allows expanding a PostgreSQL array to a set of rows; once that's done, the array values can be queried with regular SQL.

We can go further, querying for all blogs whose tags - or any of their posts' tags - contains a certain value:

var blogs = await ctx.Blogs
    .Where(b => b.Tags.Union(b.Posts.SelectMany(p => p.Tags))
        .Contains("foo"))
    .ToListAsync();

This uses unnest both on the blog's tags on and all its posts' tags (via a lateral join), to then do a UNION On results and check whether the value is contained.

SELECT b."Id", b."Tags"
FROM "Blogs" AS b
WHERE 'foo' IN (
    SELECT t.value
    FROM unnest(b."Tags") AS t(value)
    UNION
    SELECT t1.value
    FROM "Post" AS p
    JOIN LATERAL unnest(p."Tags") AS t1(value) ON TRUE
    WHERE b."Id" = p."BlogId"
)

Parameter collections

But that's not all... The new primitive collection support works not just on columns, but also on parameterized lists. Previously, special and limited support existed for translating Contains over a parameterized list, as follows:

var ids = new[] { 1, 2, 3 };

var blogs = await ctx.Blogs
    .Where(b => ids.Contains(b.Id))
    .ToListAsync();

This has now been extended to allow composing any LINQ operator. For example, the following query queries for any blogs which have at least one tag, where that tag starts with a list of possible prefixes:

var prefixes = new[] { "f", "b" };

var blogs = await ctx.Blogs
    .Where(b => prefixes.Any(p => b.Tags.Any(t => t.StartsWith(p))))
    .ToListAsync();

Complex, I know! This translates to the following SQL:

SELECT b."Id", b."Tags"
FROM "Blogs" AS b
WHERE EXISTS (
    SELECT 1
    FROM unnest(@__prefixes_0) AS p(value)
    WHERE EXISTS (
        SELECT 1
        FROM unnest(b."Tags") AS t(value)
        WHERE p.value IS NOT NULL AND left(t.value, length(p.value)) = p.value))

Note the two usages of unnest here: one is used to expand the Tags column, whereas the other is used to expand the @__prefixes_0 array parameter that EF sends. All this machinery works together to make your LINQ query execute correctly.

Inline collections

Finally, support was added to inline collections, which are collections specified inside the query. For example, let's say that the list of tag prefixes in the previous query were always the same; in that case, we'd inline the prefixes variable as follows:

var blogs = await ctx.Blogs
    .Where(b => new[] { "f", "b" }.Any(p => b.Tags.Any(t => t.StartsWith(p))))
    .ToListAsync();

This translates to the following:

      SELECT b."Id", b."Tags"
      FROM "Blogs" AS b
      WHERE EXISTS (
          SELECT 1
          FROM (VALUES ('f'::text), ('b')) AS v("Value")
          WHERE EXISTS (
              SELECT 1
              FROM unnest(b."Tags") AS t(value)
              WHERE left(t.value, length(v."Value")) = v."Value"))

Note that the unnest over the parameter has been replaced with a standard SQL VALUES construct, which allows constructing a set of rows inline, inside the query.

More information

Exciting stuff! We hope this helps you use LINQ to model and better interact with your database.

EF JSON support via ToJson()

Version 8.0 also brings support for EF7's JSON columns feature (ToJson), which allows mapping JSON columns via owned entity types. While the Npgsql EF provider has had POCO JSON mapping for a very long time, the full modeling of the new ToJson() allows for a more powerful mapping strategy, with more query and update features. ToJson is the recommended way to map JSON going forward.

You can read more on ToJson in the Npgsql JSON doc page, and in the EF7 what's new docs. Compared to the traditional Npgsql-specific POCO approach, a much wider range of LINQ queries can now be translated. For example, you can now compose LINQ operators over collections within JSON documents:

var blogs = await ctx.Customers
    .Where(b => b.Details.Orders.Any(o => o.Price > 8))
    .ToListAsync();

Note that this composes a LINQ operator - Any - on top of Orders, which is a list nested inside a JSON document. This translates to the following SQL:

SELECT c."Id", c."Details"
FROM "Customers" AS c
WHERE EXISTS (
    SELECT 1
    FROM ROWS FROM (jsonb_to_recordset(c."Details" -> 'Orders') AS (
        "Price" numeric,
        "ShippingAddress" text
    )) AS o
    WHERE o."Price" > 8.0)

Note the jsonb_to_recordset function, which drills into the JSON document, finds the Orders property, and then expands that out to a set of rows, which can then be queried via regular SQL.

Similarly, updates via EF's SaveChanges() are better, and can leverage partial updating to only patch the part of the JSON document which has changed. For example, let's assume that we load some customer with a JSON column, and change a single property within that JSON data:

var customer = await ctx.Customers.SingleAsync(c => c.Details.Name == "Foo");
customer.Details.Name = "Bar";
await ctx.SaveChangesAsync();

Rather than needlessly sending the entire JSON document back to PostgreSQL, the EF provider uses the jsonb_set function to patch the specific property which changed, which is more efficient:

UPDATE "Customers" SET "Details" = jsonb_set("Details", '{Name}', @p0)
WHERE "Id" = @p1;

Other new features

Version 8.0 contains many other smaller features and bug fixes, both on the EF side and on the Npgsql provider side. See the 8.0.0 milestone for the full list of Npgsql EF provider issues.

Breaking changes

Note: version 8.0 of the lower-level Npgsql ADO.NET driver, which is used by the EF provider, also has some breaking changes. It's recommended to read the release notes for that as well.

Because of the NativeAOT and trimming work done for Npgsql 8.0 (release notes), certain features now require an explicit opt-in, which you must add either on your xref:Npgsql.NpgsqlDataSourceBuilder or on xref:Npgsql.NpgsqlConnection.GlobalTypeMapper?displayProperty=nameWithType:

PostgreSQL type Default .NET type
JSON POCO mapping, JsonNode and subtypes xref:Npgsql.INpgsqlTypeMapperExtensions.EnableDynamicJson
Unmapped enums, ranges, multiranges xref:Npgsql.INpgsqlTypeMapperExtensions.EnableUnmappedTypes
Read PostgreSQL records as .NET tuples xref:Npgsql.INpgsqlTypeMapperExtensions.EnableRecordsAsTuples

Existing code using the above features will start throwing exceptions after upgrading to version 8.0 of the EF Core provider; the exceptions provide explicit guidance on how to add the opt-ins.

Note that EF Core itself is not yet compatible with NativeAOT, and Npgsql can only be used in NativeAOT applications without EF Core.

Default PostgreSQL compatibility mode has been bumped from 12 to 14

This means that the provider assumes at least PostgreSQL 14; if you're running against an older version, explicitly specify the PostgreSQL version when configuring your context:

optionsBuilder.UseNpgsql("<connection string>", o => o.SetPostgresVersion(12, 0))

Obsoleted HasPostgresArrayConversion

With EF 8.0 introducing first-class support for primitive collections, the PostgreSQL driver aligned its PostgreSQL array support to use that. As a result, HasPostgresArrayConversion can no longer be used to configure value-converted arrays; instead, the new standard EF mechanism can be used.

For example, the following Npgsql-specific code would configure value conversion for a property of type MyType[] to a PostgreSQL array of strings in EF Core 6 or 7:

modelBuilder.Entity<Blog>().Property(b => b.ValueConvertedArray)
    .HasPostgresArrayConversion(x => x.ToString(), s => MyType.Parse(s));

The same can now achieved with the following standard EF 8 code:

modelBuilder.Entity<Blog>().PrimitiveCollection(b => b.ValueConvertedArray)
    .ElementType()
    .HasConversion(typeof(MyConverter));

class MyConverter : ValueConverter<MyType, string>
{
    public MyConverter()
        : base(x => x.ToString(), s => MyType.Parse(s))
    {
    }
}

cidr now maps to NpgsqlCidr instead of ValueTuple<IPAddress, int>

As part of improving Npgsql's support for the PostgreSQL network mappings, the PostgreSQL cidr type now maps to the newly-introduced xref:NpgsqlTypes.NpgsqlCidr, and can no longer be mapped to ValueTuple<IPAddress, int>.

Contributors

A big thank you to all the following people who contributed to the 8.0 release!

Contributor Assigned issues
@roji 34
@zpaks 1