Skip to content

Commit 765fc44

Browse files
committed
feat: initial release of Equibles.ParadeDB.EntityFrameworkCore
EF Core integration for ParadeDB pg_search BM25 full-text search. Provides [Bm25Index] attribute and UseParadeDb() extension for automatic index creation via EF Core conventions and migrations.
0 parents  commit 765fc44

12 files changed

Lines changed: 284 additions & 0 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Publish to NuGet
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Setup .NET
16+
uses: actions/setup-dotnet@v4
17+
with:
18+
dotnet-version: '10.0.x'
19+
20+
- name: Extract version from tag
21+
id: version
22+
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
23+
24+
- name: Restore
25+
run: dotnet restore
26+
27+
- name: Build
28+
run: dotnet build -c Release --no-restore /p:Version=${{ steps.version.outputs.VERSION }}
29+
30+
- name: Pack
31+
run: dotnet pack -c Release --no-build /p:Version=${{ steps.version.outputs.VERSION }} -o ./nupkg
32+
33+
- name: Push to NuGet
34+
run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## .NET
2+
bin/
3+
obj/
4+
*.user
5+
*.suo
6+
*.userosscache
7+
*.sln.docstates
8+
9+
## NuGet
10+
*.nupkg
11+
**/[Pp]ackages/*
12+
!**/[Pp]ackages/build/
13+
14+
## IDE
15+
.vs/
16+
.idea/
17+
*.DotSettings.user
18+
19+
## OS
20+
.DS_Store
21+
Thumbs.db
22+
23+
## Claude Code
24+
.claude/

Directory.Build.props

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Version>0.1.0</Version>
7+
<Authors>Equibles, Daniel Oliveira</Authors>
8+
<Company>Equibles</Company>
9+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
10+
<PackageProjectUrl>https://github.com/daniel3303/ParadeDbEfCore</PackageProjectUrl>
11+
<RepositoryUrl>https://github.com/daniel3303/ParadeDbEfCore</RepositoryUrl>
12+
<RepositoryType>git</RepositoryType>
13+
<PackageReadmeFile>README.md</PackageReadmeFile>
14+
<PackageTags>efcore paradedb bm25 postgresql full-text-search pg_search entityframeworkcore</PackageTags>
15+
<IsPackable>true</IsPackable>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<Solution>
2+
<Project Path="Equibles.ParadeDB.EntityFrameworkCore/Equibles.ParadeDB.EntityFrameworkCore.csproj" />
3+
</Solution>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Equibles.ParadeDB.EntityFrameworkCore;
2+
3+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
4+
public sealed class Bm25IndexAttribute : Attribute {
5+
public string KeyField { get; }
6+
public string[] Columns { get; }
7+
8+
public Bm25IndexAttribute(string keyField, params string[] columns) {
9+
KeyField = keyField;
10+
Columns = [keyField, ..columns];
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Description>EF Core integration for ParadeDB pg_search BM25 full-text search indexes on PostgreSQL. Provides a [Bm25Index] attribute and UseParadeDb() extension for automatic index creation via EF Core conventions.</Description>
5+
<PackageId>Equibles.ParadeDB.EntityFrameworkCore</PackageId>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
10+
</ItemGroup>
11+
12+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
2+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
3+
4+
namespace Equibles.ParadeDB.EntityFrameworkCore;
5+
6+
public sealed class ParadeDbConventionSetPlugin : IConventionSetPlugin {
7+
public ConventionSet ModifyConventions(ConventionSet conventionSet) {
8+
conventionSet.ModelFinalizingConventions.Add(new ParadeDbModelFinalizingConvention());
9+
return conventionSet;
10+
}
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Infrastructure;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
4+
5+
namespace Equibles.ParadeDB.EntityFrameworkCore;
6+
7+
public static class ParadeDbDbContextOptionsBuilderExtensions {
8+
public static NpgsqlDbContextOptionsBuilder UseParadeDb(this NpgsqlDbContextOptionsBuilder npgsqlBuilder) {
9+
var builder = ((IRelationalDbContextOptionsBuilderInfrastructure)npgsqlBuilder).OptionsBuilder;
10+
11+
var extension = builder.Options.FindExtension<ParadeDbDbContextOptionsExtension>()
12+
?? new ParadeDbDbContextOptionsExtension();
13+
14+
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension);
15+
16+
return npgsqlBuilder;
17+
}
18+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace Equibles.ParadeDB.EntityFrameworkCore;
6+
7+
public sealed class ParadeDbDbContextOptionsExtension : IDbContextOptionsExtension {
8+
public DbContextOptionsExtensionInfo Info => new ParadeDbExtensionInfo(this);
9+
10+
public void ApplyServices(IServiceCollection services) {
11+
new EntityFrameworkRelationalServicesBuilder(services)
12+
.TryAdd<IConventionSetPlugin, ParadeDbConventionSetPlugin>();
13+
}
14+
15+
public void Validate(IDbContextOptions options) { }
16+
17+
private sealed class ParadeDbExtensionInfo : DbContextOptionsExtensionInfo {
18+
public ParadeDbExtensionInfo(IDbContextOptionsExtension extension) : base(extension) { }
19+
20+
public override bool IsDatabaseProvider => false;
21+
public override string LogFragment => "using ParadeDB ";
22+
public override int GetServiceProviderHashCode() => 0;
23+
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => other is ParadeDbExtensionInfo;
24+
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) => debugInfo["ParadeDB:BM25"] = "1";
25+
}
26+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Reflection;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
4+
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
5+
6+
namespace Equibles.ParadeDB.EntityFrameworkCore;
7+
8+
public sealed class ParadeDbModelFinalizingConvention : IModelFinalizingConvention {
9+
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context) {
10+
var hasBm25Index = false;
11+
12+
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) {
13+
var attribute = entityType.ClrType.GetCustomAttribute<Bm25IndexAttribute>();
14+
if (attribute == null) continue;
15+
16+
var indexBuilder = entityType.Builder.HasIndex(attribute.Columns, fromDataAnnotation: true);
17+
if (indexBuilder == null) continue;
18+
19+
indexBuilder.HasAnnotation("Npgsql:IndexMethod", "bm25", fromDataAnnotation: true);
20+
21+
// Resolve the key field to the actual column name
22+
var keyProperty = entityType.FindProperty(attribute.KeyField);
23+
var keyColumnName = keyProperty?.GetColumnName() ?? attribute.KeyField;
24+
indexBuilder.HasAnnotation("Npgsql:StorageParameter:key_field", keyColumnName, fromDataAnnotation: true);
25+
26+
hasBm25Index = true;
27+
}
28+
29+
if (hasBm25Index) {
30+
modelBuilder.HasAnnotation("Npgsql:PostgresExtension:pg_search", ",,", fromDataAnnotation: true);
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)