Open source·MIT licensed

Persistence contracts for modular .NET systems

Groundwork lets modules declare the entities, indexes, queries, and concurrency rules they need — without depending on EF Core or a specific database. Hosts materialize the same intent across SQLite, SQL Server, PostgreSQL, MongoDB, and other providers.

module → manifest → planner → materializer → provider
moduleApprovalRulesdeclares storageStorageManifest<T>unit: approval_rulesconcurrency: versionindexes: 3 declaredserializer: jsonPlannerSQLiterelationalSQL ServerrelationalPostgreSQLrelationalMongoDBdocument
Why Groundwork

Modular .NET frameworks need provider-neutral persistence.

Extensible platforms ship as many independent modules. Each module needs to persist data — but no module should dictate the host's database stack.

The architectural pressure

Frameworks such as Elsa, Orchard Core, and ABP all show the same pattern: modules need persistence, while host applications need freedom to choose the database provider. Groundwork gives modules a contract for storage intent that travels across providers — without a hard dependency on EF Core or one engine.

  1. 01
    Modules need entities and queries
    But they should not own connection strings, schemas, or migrations.
  2. 02
    Hosts need provider freedom
    SQLite for dev, PostgreSQL or SQL Server for production, MongoDB when documents fit.
  3. 03
    EF Core isn't always the right coupling
    Reusable modules shouldn't force every consumer onto one ORM or relational shape.
  4. 04
    Drift creeps in across backends
    The same shape restated in DDL, indexes, and migrations becomes inevitable rework.
Separation of concerns

Module author declares. Host application chooses.

Groundwork draws a clean line between what a module needs to express about its data and what the host application controls about how that data is stored.

Module author

Declares storage intent

  • Entities and field shapes
  • Queryable fields and indexes
  • Portable query semantics
  • Optimistic concurrency rules
  • Serialization preferences
  • Lifecycle policy
  • Workload classification
Host application

Chooses materialization

  • Database provider choice
  • Connection and credentials
  • Materialization timing
  • Operational and backup policy
  • Diagnostics and telemetry sinks
  • Tenant and isolation strategy
  • Optimized physicalization gates
Concrete scenario

A module defines an entity. The host picks the database.

The same pattern shows up across workflow engines, CMS platforms, integration catalogs, multi-tenant settings, and runtime-defined business objects.

A workflow, CMS, or application module defines an entity — an approval rule, a content metadata record, an integration catalog item, a tenant setting, or a runtime-defined business object.

It declares queryable fields like status, owner, tenant, key, or category. Groundwork validates the manifest against the configured provider and materializes the right storage shape — relational tables and indexes, or document collections and indexes — without the module knowing which.

module-defined entityApprovalRule
public sealed class ApprovalRuleStorage
    : StorageManifest<ApprovalRule>
{
    public ApprovalRuleStorage() : base("approval_rules")
    {
        Serialization.UseJson();
        Concurrency.UseOptimistic(r => r.Version);

        Indexes.On(r => r.TenantId);
        Indexes.On(r => r.Status);
        Indexes.On(r => r.OwnerId);
    }
}
Provider choice

Same module intent. Different materialization.

A module's storage manifest stays the same. The host picks the provider that fits the deployment — and Groundwork materializes the right physical shape for it.

Local & lightweight

SQLite

Local development, samples, integration tests, and small single-node deployments — zero infrastructure.

Relational production

SQL Server

Enterprise relational workloads with existing tooling, backups, and operational practice.

Relational production

PostgreSQL

Modern relational workloads with rich indexing and JSON support — a common default for new systems.

Document-first

MongoDB

Document-shaped workloads where flexible schemas and per-document indexes are the right fit.

EF Core coexistence

Use EF Core where it fits. Use Groundwork where modules need to stay portable.

Groundwork is not a replacement for EF Core. It's a smaller persistence contract for the parts of a system where coupling every module to one ORM is the wrong trade-off.

EF Core is great for
  • Application-owned relational domains the host fully controls.
  • Rich LINQ over a known relational schema.
  • Teams already standardized on EF Core migrations and tooling.
Groundwork is great for
  • Reusable modules that ship to many hosts on many database engines.
  • Runtime-defined entities that still need declared indexes and concurrency.
  • Keeping module packages light — no EF Core, no relational assumption.

The two can live in the same application. EF Core handles the host's core domain; Groundwork handles modules that need to remain provider-neutral. Nothing forces a choice between them.

How it works

Three steps from intent to portable storage.

Step 01

Storage intent as a manifest

Describe units, indexes, query capabilities, and concurrency through a provider-neutral StorageManifest.

Step 02

Capabilities & materialization

Providers report capabilities. Plans surface gaps and history before anything touches a database.

Step 03

Portable document contracts

Application code uses one document-store contract across SQLite, SQL Server, PostgreSQL, and MongoDB.

Features

Built for clean persistence boundaries.

Everything is declared, inspectable, and provider-aware — without surrendering provider freedom.

Provider-neutral storage manifests

Declare units, fields, and contracts once — independent of any database engine.

Declared indexes & portable queries

Query semantics travel with the manifest. Unindexed portable queries fail clearly.

Optimistic concurrency

Version tokens are first-class — surfaced consistently across providers.

Provider capability validation

Each provider reports what it supports. Manifests are checked before execution.

Materialization & schema history

Plans are explicit, inspectable artifacts — never silent runtime side effects.

Portable document-store contract

One contract for document access across relational and document backends.

Four first-party providers

SQLite, SQL Server, PostgreSQL, and MongoDB ship as discrete provider packages.

Opt-in optimized physicalization

Promote hot indexed paths to provider-native shapes, benchmark-gated and explicit.

In code

A manifest that travels across providers.

One declaration; the same indexes, concurrency, and serialization across every supported backend.

SupportTicketStorage.cs
C#
1// Declare storage intent once — portable across providers.
2public sealed class SupportTicketStorage : StorageManifest<SupportTicket>
3{
4 public SupportTicketStorage() : base("support_tickets")
5 {
6 Serialization.UseJson();
7 Concurrency.UseOptimistic(t => t.Version);
8 
9 // Declared indexes — portable query semantics
10 Indexes.Unique(t => t.TicketNumber);
11 Indexes.On(t => t.CustomerId);
12 Indexes.On(t => t.Status);
13 Indexes.On(t => t.AssigneeId);
14 Indexes.On(t => t.Priority);
15 }
16}
Providers

Four first-party providers. Equal treatment, honest differences.

Each provider package translates manifests into its native shape — and reports honestly about its capabilities.

Capability
SQLite
SQL Server
PostgreSQL
MongoDB
Portable document storage
Single contract across relational and document backends.
native
Declared indexes
Index intent declared in the manifest; enforced per provider.
Materialization & history
Plans and schema history are first-class, inspectable artifacts.
Optimized physicalization
Opt-in promotion of hot paths to provider-native shapes.
Example

A support-ticket system, written once.

The same manifest carries from local SQLite through production PostgreSQL or MongoDB — without touching the domain.

  1. 01
    Declare
    Define ticket storage in one StorageManifest — fields, indexes, concurrency.
  2. 02
    Materialize locally
    Run against SQLite for development and tests with zero infrastructure.
  3. 03
    Promote backend
    Move to PostgreSQL or MongoDB without changing the domain contract.
  4. 04
    Query by index
    All access goes through declared indexes — portable across providers.
  5. 05
    Preserve concurrency
    Optimistic version tokens travel with the manifest, not the provider.
same call site, any provider
var ticket = await store.GetByIndexAsync(
    t => t.TicketNumber,
    "TCK-10472"
);

ticket.Status = TicketStatus.Resolved;

await store.UpdateAsync(ticket); // optimistic
Runs unchanged on:
SQLite PG Mongo
Architecture

Principles that shape every package.

Groundwork's value comes from what it refuses to do as much as what it offers.

P.01
Generic packages stay application-agnostic
Core Groundwork assemblies never know about your domain. They define the contract, not the content.
P.02
Provider-specific shape lives in providers
Application modules never reach for SQL or document syntax. Provider packages own physical translation.
P.03
Unindexed portable queries fail clearly
Surprises are worse than errors. Portable queries refuse to silently degrade.
P.04
Runtime hot paths stay benchmark-gated
Optimized physicalization is explicit, measured, and opt-in — never accidental.
P.05
Application integrations remain opt-in
Groundwork integrates where you choose it to. It does not colonize the rest of your stack.
Open source

Built in the open, designed for adoption.

Groundwork is open source and MIT licensed, so teams can inspect the implementation, extend providers, fork when needed, and embed it in their own .NET systems without vendor lock-in.

Inspectable
Read the implementation. No hidden runtime.
Forkable
Adapt or extend providers when your stack needs it.
MIT licensed
Embed it in commercial systems without lock-in.

Build persistence on declared intent.

Use Groundwork as an MIT-licensed foundation for provider-neutral persistence in .NET applications.