From 4616510be003393588788e213b992fac6d2064ee Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 16 Oct 2026 15:57:53 +0200 Subject: [PATCH] Prepare EF Core 11 release and vector auto-load convention Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae01fae1-3e57-4d55-ad2c-f746a2d1084d --- .github/workflows/build.yml | 3 + README.md | 23 ++++-- global.json | 4 +- src/Pgvector.EntityFrameworkCore/CHANGELOG.md | 6 ++ .../Pgvector.EntityFrameworkCore.csproj | 6 +- .../VectorConventionSetPlugin.cs | 31 ++++++++ .../VectorDbContextOptionsExtension.cs | 2 + .../Pgvector.EntityFrameworkCore.targets | 0 .../EntityFrameworkCoreTests.cs | 78 ++++++++++++++++++- .../Pgvector.CSharp.Tests.csproj | 2 +- 10 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs rename src/Pgvector.EntityFrameworkCore/build/{net8.0 => net11.0}/Pgvector.EntityFrameworkCore.targets (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac896a8..a2fabbd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,9 @@ jobs: steps: - uses: actions/checkout@v7 - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 11.0.x + dotnet-quality: preview - uses: ankane/setup-postgres@v1 with: database: pgvector_dotnet_test diff --git a/README.md b/README.md index 0f3b7b2..da7cae5 100644 --- a/README.md +++ b/README.md @@ -191,10 +191,10 @@ See a [full example](https://github.com/pgvector/pgvector-dotnet/blob/master/tes Run ```sh -dotnet add package Pgvector.EntityFrameworkCore +dotnet add package Pgvector.EntityFrameworkCore --version 0.4.0-rc.1 ``` -The latest version works with Entity Framework Core 9 and 10. For Entity Framework Core 8, use version 0.2.2 and [this readme](https://github.com/pgvector/pgvector-dotnet/blob/efcore-v0.2.2/README.md#entity-framework-core). +Version 0.4.0-rc.1 requires .NET 11 and works with Entity Framework Core 11 (currently Npgsql.EntityFrameworkCore.PostgreSQL 11.0.0-rc.1.1). This is a release candidate, as Entity Framework Core 11 has not yet been released. For Entity Framework Core 9 and 10, use version 0.3.0. For Entity Framework Core 8, use version 0.2.2 and [this readme](https://github.com/pgvector/pgvector-dotnet/blob/efcore-v0.2.2/README.md#entity-framework-core). Import the library @@ -247,6 +247,20 @@ public class Item Also supports `HalfVector` and `SparseVector` +Vector properties are not automatically loaded when querying entities. They remain available for filtering, ordering, and explicit projections: + +```csharp +var embeddings = await ctx.Items.Select(x => x.Embedding).ToListAsync(); +``` + +To automatically load a vector property, configure it explicitly in `OnModelCreating`: + +```csharp +modelBuilder.Entity().Property(x => x.Embedding).Metadata.IsAutoLoaded = true; +``` + +This also applies to `HalfVector` and `SparseVector`, but not `BitArray` properties. + Insert a vector ```csharp @@ -265,10 +279,7 @@ var items = await ctx.Items foreach (Item item in items) { - if (item.Embedding != null) - { - Console.WriteLine(item.Embedding); - } + Console.WriteLine(item.Id); } ``` diff --git a/global.json b/global.json index 0b2a45c..b40b33e 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.100", + "version": "11.0.100-rc.2.26504.105", "rollForward": "latestMajor", - "allowPrerelease": "false" + "allowPrerelease": true } } diff --git a/src/Pgvector.EntityFrameworkCore/CHANGELOG.md b/src/Pgvector.EntityFrameworkCore/CHANGELOG.md index c08d9f7..9edf4f3 100644 --- a/src/Pgvector.EntityFrameworkCore/CHANGELOG.md +++ b/src/Pgvector.EntityFrameworkCore/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.4.0-rc.1 (unreleased) + +- Updated Npgsql.EntityFrameworkCore.PostgreSQL to 11.0.0-rc.1.1 +- Dropped support for .NET < 11 +- Disabled automatic loading of `Vector`, `HalfVector`, and `SparseVector` properties by default + ## 0.3.0 (2025-12-20) - Improved type registration diff --git a/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj b/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj index 34a1ec5..e6b33a1 100644 --- a/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj +++ b/src/Pgvector.EntityFrameworkCore/Pgvector.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ Pgvector.EntityFrameworkCore - 0.3.0 + 0.4.0-rc.1 ankane pgvector support for Entity Framework Core MIT @@ -10,7 +10,7 @@ README.md icon.png - net8.0 + net11.0 enable enable latest @@ -22,7 +22,7 @@ - + diff --git a/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs b/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs new file mode 100644 index 0000000..4da8f78 --- /dev/null +++ b/src/Pgvector.EntityFrameworkCore/VectorConventionSetPlugin.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Pgvector.EntityFrameworkCore; + +public class VectorConventionSetPlugin : IConventionSetPlugin +{ + private readonly ProviderConventionSetBuilderDependencies _dependencies; + + public VectorConventionSetPlugin(ProviderConventionSetBuilderDependencies dependencies) + { + _dependencies = dependencies; + } + + public virtual ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.Add(new VectorAutoLoadConvention(_dependencies)); + return conventionSet; + } + + private class VectorAutoLoadConvention : AutoLoadConvention + { + public VectorAutoLoadConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { } + + protected override bool ShouldBeAutoLoaded(IConventionProperty property) + => property.ClrType != typeof(Vector) + && property.ClrType != typeof(HalfVector) + && property.ClrType != typeof(SparseVector); + } +} diff --git a/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs b/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs index 5da3dca..5a72904 100644 --- a/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs +++ b/src/Pgvector.EntityFrameworkCore/VectorDbContextOptionsExtension.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; @@ -16,6 +17,7 @@ public void ApplyServices(IServiceCollection services) { new EntityFrameworkNpgsqlServicesBuilder(services) .TryAdd() + .TryAdd() .TryAdd(); services.AddSingleton(); diff --git a/src/Pgvector.EntityFrameworkCore/build/net8.0/Pgvector.EntityFrameworkCore.targets b/src/Pgvector.EntityFrameworkCore/build/net11.0/Pgvector.EntityFrameworkCore.targets similarity index 100% rename from src/Pgvector.EntityFrameworkCore/build/net8.0/Pgvector.EntityFrameworkCore.targets rename to src/Pgvector.EntityFrameworkCore/build/net11.0/Pgvector.EntityFrameworkCore.targets diff --git a/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs b/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs index a896f6b..96b16f9 100644 --- a/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs +++ b/tests/Pgvector.CSharp.Tests/EntityFrameworkCoreTests.cs @@ -50,10 +50,21 @@ public class Item public class EntityFrameworkCoreTests { + private class AutoLoadedItemContext : ItemContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().Property(x => x.Embedding).Metadata.IsAutoLoaded = true; + modelBuilder.Entity().Property(x => x.HalfEmbedding).Metadata.IsAutoLoaded = true; + modelBuilder.Entity().Property(x => x.SparseEmbedding).Metadata.IsAutoLoaded = true; + } + } + [Fact] public async Task Main() { - await using var ctx = new ItemContext(); + await using var ctx = new AutoLoadedItemContext(); ctx.Database.ExecuteSql($"DROP TABLE IF EXISTS efcore_items"); var databaseCreator = ctx.GetService(); @@ -63,6 +74,7 @@ public async Task Main() ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 2, 2, 2 }), HalfEmbedding = new HalfVector(new Half[] { (Half)2, (Half)2, (Half)2 }), BinaryEmbedding = new BitArray(new bool[] { true, false, true }), SparseEmbedding = new SparseVector(new float[] { 2, 2, 2 }) }); ctx.Items.Add(new Item { Embedding = new Vector(new float[] { 1, 1, 2 }), HalfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)1, (Half)2 }), BinaryEmbedding = new BitArray(new bool[] { true, true, true }), SparseEmbedding = new SparseVector(new float[] { 1, 1, 2 }) }); ctx.SaveChanges(); + ctx.ChangeTracker.Clear(); var embedding = new Vector(new float[] { 1, 1, 1 }); var items = await ctx.Items.FromSql($"SELECT * FROM efcore_items ORDER BY embedding <-> {embedding} LIMIT 5").ToListAsync(); @@ -142,6 +154,70 @@ public async Task Main() Assert.Equal(new double[] { 0, 1, Math.Sqrt(3) }, neighbors.Select(v => v.Distance).ToArray()); } + [Fact] + public async Task VectorPropertiesAreNotAutoLoaded() + { + await using var ctx = new ItemContext(); + var entityType = ctx.Model.FindEntityType(typeof(Item))!; + Assert.False(entityType.FindProperty(nameof(Item.Embedding))!.IsAutoLoaded); + Assert.False(entityType.FindProperty(nameof(Item.HalfEmbedding))!.IsAutoLoaded); + Assert.False(entityType.FindProperty(nameof(Item.SparseEmbedding))!.IsAutoLoaded); + Assert.True(entityType.FindProperty(nameof(Item.Id))!.IsAutoLoaded); + Assert.True(entityType.FindProperty(nameof(Item.BinaryEmbedding))!.IsAutoLoaded); + + await ctx.Database.ExecuteSqlAsync($"DROP TABLE IF EXISTS efcore_items"); + ctx.GetService().CreateTables(); + + var embedding = new Vector(new float[] { 1, 2, 3 }); + var halfEmbedding = new HalfVector(new Half[] { (Half)1, (Half)2, (Half)3 }); + var sparseEmbedding = new SparseVector(new float[] { 1, 2, 3 }); + var binaryEmbedding = new BitArray(new bool[] { true, false, true }); + ctx.Items.Add(new Item + { + Embedding = embedding, + HalfEmbedding = halfEmbedding, + SparseEmbedding = sparseEmbedding, + BinaryEmbedding = binaryEmbedding + }); + await ctx.SaveChangesAsync(); + ctx.ChangeTracker.Clear(); + + var sql = ctx.Items.ToQueryString(); + Assert.DoesNotContain(".embedding", sql); + Assert.DoesNotContain(".half_embedding", sql); + Assert.DoesNotContain(".sparse_embedding", sql); + Assert.Contains(".binary_embedding", sql); + + var untracked = await ctx.Items.AsNoTracking().SingleAsync(); + Assert.Null(untracked.Embedding); + Assert.Null(untracked.HalfEmbedding); + Assert.Null(untracked.SparseEmbedding); + Assert.Equal(binaryEmbedding, untracked.BinaryEmbedding); + + var neighbor = await ctx.Items + .Where(x => x.Embedding!.L2Distance(embedding) < 1) + .OrderBy(x => x.Embedding!.L2Distance(embedding)) + .Select(x => new { Entity = x, Distance = x.Embedding!.L2Distance(embedding) }) + .SingleAsync(); + Assert.Equal(untracked.Id, neighbor.Entity.Id); + Assert.Equal(0, neighbor.Distance); + Assert.Null(neighbor.Entity.Embedding); + Assert.Null(neighbor.Entity.HalfEmbedding); + Assert.Null(neighbor.Entity.SparseEmbedding); + Assert.Equal(binaryEmbedding, neighbor.Entity.BinaryEmbedding); + + neighbor.Entity.BinaryEmbedding = new BitArray(new bool[] { false, true, false }); + await ctx.SaveChangesAsync(); + + var projected = await ctx.Items + .Select(x => new { x.Embedding, x.HalfEmbedding, x.SparseEmbedding, x.BinaryEmbedding }) + .SingleAsync(); + Assert.Equal(embedding, projected.Embedding); + Assert.Equal(halfEmbedding, projected.HalfEmbedding); + Assert.Equal(sparseEmbedding, projected.SparseEmbedding); + Assert.Equal(neighbor.Entity.BinaryEmbedding, projected.BinaryEmbedding); + } + [Theory] [InlineData(typeof(Vector), null, "vector")] [InlineData(typeof(Vector), 3, "vector(3)")] diff --git a/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj b/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj index f25be28..522ee1a 100644 --- a/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj +++ b/tests/Pgvector.CSharp.Tests/Pgvector.CSharp.Tests.csproj @@ -1,7 +1,7 @@ - net10.0 + net11.0 enable enable