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.
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.
- 01Modules need entities and queriesBut they should not own connection strings, schemas, or migrations.
- 02Hosts need provider freedomSQLite for dev, PostgreSQL or SQL Server for production, MongoDB when documents fit.
- 03EF Core isn't always the right couplingReusable modules shouldn't force every consumer onto one ORM or relational shape.
- 04Drift creeps in across backendsThe same shape restated in DDL, indexes, and migrations becomes inevitable rework.
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.
Declares storage intent
- Entities and field shapes
- Queryable fields and indexes
- Portable query semantics
- Optimistic concurrency rules
- Serialization preferences
- Lifecycle policy
- Workload classification
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
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.
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);
}
}
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.
SQLite
Local development, samples, integration tests, and small single-node deployments — zero infrastructure.
SQL Server
Enterprise relational workloads with existing tooling, backups, and operational practice.
PostgreSQL
Modern relational workloads with rich indexing and JSON support — a common default for new systems.
MongoDB
Document-shaped workloads where flexible schemas and per-document indexes are the right fit.
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.
- Application-owned relational domains the host fully controls.
- Rich LINQ over a known relational schema.
- Teams already standardized on EF Core migrations and tooling.
- 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.
Three steps from intent to portable storage.
Storage intent as a manifest
Describe units, indexes, query capabilities, and concurrency through a provider-neutral StorageManifest.
Capabilities & materialization
Providers report capabilities. Plans surface gaps and history before anything touches a database.
Portable document contracts
Application code uses one document-store contract across SQLite, SQL Server, PostgreSQL, and MongoDB.
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.
A manifest that travels across providers.
One declaration; the same indexes, concurrency, and serialization across every supported backend.
1// Declare storage intent once — portable across providers.2public sealed class SupportTicketStorage : StorageManifest<SupportTicket>3{4public SupportTicketStorage() : base("support_tickets")5{6Serialization.UseJson();7Concurrency.UseOptimistic(t => t.Version);89// Declared indexes — portable query semantics10Indexes.Unique(t => t.TicketNumber);11Indexes.On(t => t.CustomerId);12Indexes.On(t => t.Status);13Indexes.On(t => t.AssigneeId);14Indexes.On(t => t.Priority);15}16}
Four first-party providers. Equal treatment, honest differences.
Each provider package translates manifests into its native shape — and reports honestly about its capabilities.
A support-ticket system, written once.
The same manifest carries from local SQLite through production PostgreSQL or MongoDB — without touching the domain.
- 01DeclareDefine ticket storage in one StorageManifest — fields, indexes, concurrency.
- 02Materialize locallyRun against SQLite for development and tests with zero infrastructure.
- 03Promote backendMove to PostgreSQL or MongoDB without changing the domain contract.
- 04Query by indexAll access goes through declared indexes — portable across providers.
- 05Preserve concurrencyOptimistic version tokens travel with the manifest, not the provider.
var ticket = await store.GetByIndexAsync(
t => t.TicketNumber,
"TCK-10472"
);
ticket.Status = TicketStatus.Resolved;
await store.UpdateAsync(ticket); // optimistic
Principles that shape every package.
Groundwork's value comes from what it refuses to do as much as what it offers.
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.
Build persistence on declared intent.
Use Groundwork as an MIT-licensed foundation for provider-neutral persistence in .NET applications.