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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EFRadix.Core.Postgres" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EFRadix.Core.Postgres" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="EFRadix.Core.Postgres" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EFRadix.Core.Postgres --version 1.0.4
                    
#r "nuget: EFRadix.Core.Postgres, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EFRadix.Core.Postgres@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EFRadix.Core.Postgres&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=EFRadix.Core.Postgres&version=1.0.4
                    
Install as a Cake Tool

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

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);
}

IAppRepositoryContext is generated at compile time by the EFRadix source generator from your DbSet properties. 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 EnableLegacyTimestampBehavior and DisableDateTimeInfinityConversions based on NpgsqlDataOptions.
  • Entity configuration discovery — calls modelBuilder.ApplyConfigurationsFromAssembly(...) so all IEntityTypeConfiguration<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 extending IRepositoryContext with a named property per entity.
  • {Name}RepositoryContext.g.cs — a partial class implementing the interface, with each property delegating to Repository<T>().
  • {Name}DbContextRegistration.g.cs — a [ModuleInitializer] that registers the DbContext → RepositoryContext mapping in EFRadixContextRegistry automatically at app startup, so AddEFRadixNpgsqlDataContext<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:

  • AppDbContext as a scoped DbContext backed by an NpgsqlDataSource.
  • IAppRepositoryContextAppRepositoryContext (scoped).
  • IRepositoryContext → resolves via IAppRepositoryContext (scoped).
  • The BaseRepository<,> open generic in EFRadixRepositoryRegistry for 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 SaveChangesAsync internally. This keeps the API simple but means each operation is its own transaction. Use SaveChangesAsync() explicitly if you need to batch multiple tracked changes into a single transaction.
  • No-tracking readsGetQueryable() and GetByIdAsync() use AsNoTracking() for performance. Entities returned from these methods are not tracked by the context.
  • Automatic DI wiring — the [ModuleInitializer] emitted by the generator runs before Main, so the DbContext → RepositoryContext mapping is in place before any DI registration code executes.
  • Provider registryEFRadixRepositoryRegistry decouples the core RepositoryContext from any specific provider. Provider packages register their BaseRepository<,> 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.0.4 156 4/7/2026
1.0.2 103 4/7/2026
1.0.1 89 4/7/2026
1.0.0 96 4/7/2026