EFRadix.Core.Postgres
1.0.4
dotnet add package EFRadix.Core.Postgres --version 1.0.4
NuGet\Install-Package EFRadix.Core.Postgres -Version 1.0.4
<PackageReference Include="EFRadix.Core.Postgres" Version="1.0.4" />
<PackageVersion Include="EFRadix.Core.Postgres" Version="1.0.4" />
<PackageReference Include="EFRadix.Core.Postgres" />
paket add EFRadix.Core.Postgres --version 1.0.4
#r "nuget: EFRadix.Core.Postgres, 1.0.4"
#:package EFRadix.Core.Postgres@1.0.4
#addin nuget:?package=EFRadix.Core.Postgres&version=1.0.4
#tool nuget:?package=EFRadix.Core.Postgres&version=1.0.4
EFRadix.Core.Postgres
A PostgreSQL data access SDK for .NET 8 built on top of Entity Framework Core and Npgsql. Provides a generic repository pattern, automatic CRUD operations, and a Roslyn source generator that emits typed repository context classes from your DbContext at compile time — zero boilerplate, full IntelliSense.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Source Generator
- Registration
- Entity Configuration
- Migrations
- Advanced Usage
- Design Notes
Installation
dotnet add package EFRadix.Core.Postgres
Quick Start
1. Define your entities
using EFRadix.Core.Postgres.Entities;
public class Student : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
2. Define your DbContext
using EFRadix.Core.Postgres.Contexts;
using Microsoft.EntityFrameworkCore;
public class AppDbContext(DbContextOptions<AppDbContext> options)
: BaseNpgsqlDbContext<AppDbContext>(options)
{
public DbSet<Student> Students { get; set; }
}
3. Register in DI
// Program.cs — just the DbContext type, no generated type names required
builder.Services.AddEFRadixNpgsqlDataContext<AppDbContext>(builder.Configuration);
4. Inject and use
public class StudentService(IAppRepositoryContext db)
{
public Task<Student?> GetAsync(string id) =>
db.StudentRepository.GetByIdAsync(id);
public Task CreateAsync(Student student) =>
db.StudentRepository.AddAsync(student);
}
IAppRepositoryContextis generated at compile time by the EFRadix source generator from yourDbSetproperties. The DI wiring is handled automatically — you never reference the generated class directly.
Core Concepts
BaseEntity
All entities must inherit from BaseEntity:
public abstract class BaseEntity
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Id— a GUID string, auto-generated on construction.CreatedAt— UTC timestamp, set on construction.
Extend with additional audit fields (UpdatedAt, CreatedBy, etc.) as needed.
IBaseRepository<T>
The generic CRUD contract available for every entity:
| Method | Description |
|---|---|
GetQueryable() |
Returns a no-tracking IQueryable<T> for composing custom LINQ queries |
GetByIdAsync(id) |
Fetches a single entity by primary key; returns null if not found |
AddAsync(entity) |
Inserts and immediately saves |
AddRangeAsync(entities) |
Bulk insert and immediately saves |
UpdateAsync(entity) |
Updates and immediately saves |
RemoveAsync(entity) |
Deletes and immediately saves |
RemoveRangeAsync(entities) |
Bulk delete and immediately saves |
SaveChangesAsync() |
Explicitly flushes all pending changes on the context |
All write operations call SaveChangesAsync internally. Use the explicit SaveChangesAsync() when batching multiple tracked changes manually.
IRepositoryContext
The base entry point for repository access:
public interface IRepositoryContext
{
IBaseRepository<T> Repository<T>() where T : BaseEntity;
}
The source generator extends this with named typed properties, giving you:
public interface IAppRepositoryContext : IRepositoryContext
{
IBaseRepository<Student> StudentRepository { get; }
IBaseRepository<Tenant> TenantRepository { get; }
}
Inject IAppRepositoryContext (the generated interface) rather than IRepositoryContext to get named typed properties and full IntelliSense.
BaseNpgsqlDbContext<T>
The base class for your DbContext. It handles two things automatically:
- Npgsql switches — configures
EnableLegacyTimestampBehaviorandDisableDateTimeInfinityConversionsbased onNpgsqlDataOptions. - Entity configuration discovery — calls
modelBuilder.ApplyConfigurationsFromAssembly(...)so allIEntityTypeConfiguration<T>implementations in your assembly are applied without manual registration.
public class AppDbContext(DbContextOptions<AppDbContext> options)
: BaseNpgsqlDbContext<AppDbContext>(options)
{
public DbSet<Student> Students { get; set; }
public DbSet<Tenant> Tenants { get; set; }
}
Source Generator
EFRadix includes a Roslyn incremental source generator that runs at compile time. It scans your DbContext for DbSet<T> properties and emits three files:
I{Name}RepositoryContext.g.cs— a typed interface extendingIRepositoryContextwith a named property per entity.{Name}RepositoryContext.g.cs— a partial class implementing the interface, with each property delegating toRepository<T>().{Name}DbContextRegistration.g.cs— a[ModuleInitializer]that registers theDbContext → RepositoryContextmapping inEFRadixContextRegistryautomatically at app startup, soAddEFRadixNpgsqlDataContext<TContext>can wire everything up without you referencing the generated class.
Example — given AppDbContext with DbSet<Student> and DbSet<Tenant>, the generator produces:
// IAppRepositoryContext.g.cs
public interface IAppRepositoryContext : IRepositoryContext
{
IBaseRepository<Student> StudentRepository { get; }
IBaseRepository<Tenant> TenantRepository { get; }
}
// AppRepositoryContext.g.cs
public partial class AppRepositoryContext
: RepositoryContext<AppDbContext>, IAppRepositoryContext
{
public AppRepositoryContext(AppDbContext context) : base(context) { }
public IBaseRepository<Student> StudentRepository => Repository<Student>();
public IBaseRepository<Tenant> TenantRepository => Repository<Tenant>();
}
// AppDbContextRegistration.g.cs
internal static class AppDbContextRegistration
{
[ModuleInitializer]
public static void Register()
=> EFRadixContextRegistry.Register(typeof(AppDbContext), typeof(AppRepositoryContext), typeof(IAppRepositoryContext));
}
The generated files appear under Analyzers in your project tree. No attributes or additional configuration are required — the generator detects any class inheriting from BaseNpgsqlDbContext<T> automatically.
Registration
AddEFRadixNpgsqlDataContext
Register your DbContext with a single type parameter — the generated RepositoryContext is resolved automatically via the [ModuleInitializer] emitted by the source generator:
builder.Services.AddEFRadixNpgsqlDataContext<AppDbContext>(builder.Configuration);
With options:
builder.Services.AddEFRadixNpgsqlDataContext<AppDbContext>(
builder.Configuration,
options =>
{
options.ConnectionStringName = "DbConnection";
options.UseJsonNet = true;
});
This registers:
AppDbContextas a scopedDbContextbacked by anNpgsqlDataSource.IAppRepositoryContext→AppRepositoryContext(scoped).IRepositoryContext→ resolves viaIAppRepositoryContext(scoped).- The
BaseRepository<,>open generic inEFRadixRepositoryRegistryfor runtime resolution.
NpgsqlDataOptions
| Property | Type | Default | Description |
|---|---|---|---|
ConnectionStringName |
string |
"DbConnection" |
Key used to resolve the connection string from IConfiguration |
UseJsonNet |
bool |
false |
Enables Newtonsoft.Json support for JSON columns |
EnableLegacyTimestampBehavior |
bool |
true |
Disables Npgsql's UTC-only enforcement for DateTime values |
DisableDateTimeInfinityConversions |
bool |
true |
Disables mapping of DateTime.MinValue/MaxValue to -infinity/infinity |
Connection string in appsettings.json:
{
"ConnectionStrings": {
"DbConnection": "Host=localhost;Database=mydb;Username=postgres;Password=secret"
}
}
Entity Configuration
Place IEntityTypeConfiguration<T> implementations in the same assembly as your DbContext. They are discovered and applied automatically.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.HasKey(s => s.Id);
builder.Property(s => s.Name).IsRequired().HasMaxLength(200);
builder.Property(s => s.Email).IsRequired().HasMaxLength(200);
}
}
Migrations
The source generator emits a named migration method per DbContext — Run{Prefix}MigrationsAsync — as an extension on IHost. This works for both ASP.NET Core web applications and .NET worker services.
Basic usage
// Program.cs
await app.RunAppMigrationsAsync();
With data seeding
Pass an optional Func<IServiceProvider, Task> callback. It is only invoked when at least one migration was actually applied:
await app.RunAppMigrationsAsync(async sp =>
{
var db = sp.GetRequiredService<IAppRepositoryContext>();
await db.StudentRepository.AddAsync(new Student { Name = "Admin" });
});
Multiple DbContexts
Each context gets its own method:
await app.RunAppMigrationsAsync();
await app.RunOrdersMigrationsAsync();
Worker service
Since the extension is on IHost, it works identically in a worker:
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services => { ... })
.Build();
await host.RunAppMigrationsAsync();
await host.RunAsync();
Migration output is logged under the TContext category, so log filtering works per-context.
Advanced Usage
Custom Queries with GetQueryable
GetQueryable() returns a no-tracking IQueryable<T>, which you can compose with standard LINQ:
var students = await db.StudentRepository
.GetQueryable()
.Where(s => s.Name.Contains("John"))
.OrderBy(s => s.CreatedAt)
.ToListAsync();
For projections, joins, or pagination, compose directly on the queryable before materializing.
WhereIf
WhereIf is a conditional filter extension on IQueryable<T>. It applies the predicate only when the condition is true, otherwise returns the source unchanged. Useful for building dynamic queries without nested if statements.
public IQueryable<Student> Filter(string? name, bool activeOnly)
{
return db.StudentRepository
.GetQueryable()
.WhereIf(!string.IsNullOrEmpty(name), s => s.Name.Contains(name!))
.WhereIf(activeOnly, s => s.IsActive);
}
Signature:
IQueryable<T> WhereIf<T>(this IQueryable<T> source, bool condition, Expression<Func<T, bool>> predicate)
Multiple DbContexts
Each DbContext gets its own registration call. The source generator emits separate files for each one — there are no naming conflicts:
builder.Services.AddEFRadixNpgsqlDataContext<AppDbContext>(builder.Configuration);
builder.Services.AddEFRadixNpgsqlDataContext<OrdersDbContext>(builder.Configuration);
Inject each by its generated interface:
public class OrderService(IOrdersRepositoryContext db) { ... }
public class StudentService(IAppRepositoryContext db) { ... }
Using Without the Source Generator
If you prefer not to use the source generator, register DynamicRepositoryContext<TContext> manually and access repositories via the generic Repository<T>() method:
// Registration
services.AddDbContext<AppDbContext>(...);
services.AddScoped<IRepositoryContext>(sp =>
new DynamicRepositoryContext<AppDbContext>(sp.GetRequiredService<AppDbContext>()));
// Usage — no named properties, generic access only
var repo = repositoryContext.Repository<Student>();
await repo.AddAsync(student);
Note: this path does not provide named typed properties or IntelliSense for individual repositories.
Design Notes
- Immediate persistence — every write method calls
SaveChangesAsyncinternally. This keeps the API simple but means each operation is its own transaction. UseSaveChangesAsync()explicitly if you need to batch multiple tracked changes into a single transaction. - No-tracking reads —
GetQueryable()andGetByIdAsync()useAsNoTracking()for performance. Entities returned from these methods are not tracked by the context. - Automatic DI wiring — the
[ModuleInitializer]emitted by the generator runs beforeMain, so theDbContext → RepositoryContextmapping is in place before any DI registration code executes. - Provider registry —
EFRadixRepositoryRegistrydecouples the coreRepositoryContextfrom any specific provider. Provider packages register theirBaseRepository<,>type at startup, keeping the core layer provider-agnostic. - Compile-time generation — the source generator uses Roslyn's incremental pipeline, so regeneration is cache-aware and does not affect IDE responsiveness.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.6)
- Microsoft.EntityFrameworkCore.UnitOfWork (>= 3.1.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Npgsql (>= 8.0.3)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 8.0.4)
- Npgsql.Json.NET (>= 8.0.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on EFRadix.Core.Postgres:
| Package | Downloads |
|---|---|
|
DotnetCqrsPgTemplate.Api
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.