Table of Contents

Getting started — PostgreSQL

The PostgreSQL provider implements the eQuantic.Core.Data contracts directly on Npgsql — no Entity Framework anywhere in the stack.

1. Install

dotnet add package eQuantic.Core.Data.PostgreSql

2. Define an entity

An entity is a plain class implementing IEntity<TKey>. No base class is required, and no driver attributes ever appear on it:

using eQuantic.Core.Data.Repository;

public sealed class Product : IEntity<Guid>
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; } = "";
    public string Category { get; set; } = "";
    public decimal Price { get; set; }
    public int Quantity { get; set; }

    public Guid GetKey() => Id;
    public void SetKey(Guid key) => Id = key;
}

By convention, the member named Id is the key and column names go through the dialect's naming convention (Namename, CreatedAtcreated_at on PostgreSQL). Everything is overridable — see Modeling.

3. Register

services.AddPostgreSqlDatabase(connectionString, model => model
    .Entity<Product>(_ => { }));          // conventions cover this entity entirely
services.AddPostgreSqlRepositories();

// optional: typed, fluent migrations (see the Migrations guide)
services.AddPostgreSqlMigrations(typeof(Program).Assembly);

AddPostgreSqlRepositories() registers the open generics — IRepository<,>, IAsyncRepository<,>, IQueryableRepository<,>, IAsyncQueryableRepository<,> — over a scoped unit of work, so any entity registered in the model resolves a repository with zero classes written by you.

4. Create the schema

Schema lives in migrations — typed, fluent, derived from the model:

[Migration("Products setup", 2026, 7, 22, 12, 0, 0)]
public sealed class ProductsSetup : Migration
{
    public override void Up(IMigrationBuilder migration) => migration
        .For<Product>(product => product
            .EnsureCollection()            // CREATE TABLE from the model
            .Index(x => x.Category));
}
// on startup:
await scope.ServiceProvider.GetRequiredService<IMigrationRunner>().RunAsync();

5. Read and write

var repository = scope.ServiceProvider.GetRequiredService<IAsyncRepository<Product, Guid>>();
var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();

// writes stage on the unit of work; one atomic batched flush runs on Commit
await repository.AddAsync(new Product { Name = "Keyboard", Category = "Peripherals", Price = 49.90m });
await repository.AddAsync(new Product { Name = "Mouse", Category = "Peripherals", Price = 19.90m });
await unitOfWork.CommitAsync();           // one transaction, generated keys read back

// reads
var peripherals = await repository.GetFilteredAsync(
    p => p.Category == "Peripherals" && p.Price < 30m,
    new QueryOptions<Product>().OrderBy(p => p.Price));

var page = await repository.GetPagedAsync(new PageRequest(pageIndex: 1, pageSize: 20),
    new QueryOptions<Product>().OrderBy(p => p.Name));

// set-based writes run immediately (no entity loading)
await repository.UpdateManyAsync(
    p => p.Category == "Peripherals",
    p => new Product { Quantity = p.Quantity + 10 });

On PostgreSQL the commit is atomic: one batched flush inside a transaction, with generated keys read back through RETURNING.

Where next

  • The contracts — what each interface is for.
  • Modeling — annotations, fluent, conventions, [Facet], composite keys.
  • PostgreSQL deep dive — jsonb, GIN/trigram indexes, filtered indexes, RETURNING, resilience.