SQL lives in XML files with a schema, addressed by group and name, and executed with Dapper — so a query can be reviewed, reused across services, and written per database engine without the calling code knowing which engine it reached.
ApricotFramework.DataOps.Abstractions is the zero-dependency contract.
dotnet add package ApricotFramework.DataOps.AspNetCore
dotnet add package ApricotFramework.DataOps.Abstractions # for a library that only calls IDataOperations<!-- Operations/Authors.xml, embedded or deployed as a file -->
<DataOperations>
<DataConfiguration Compatibility="Sqlite" Timeout="PT30S" />
<OperationGroup Name="Authors">
<SqlOperation Name="All">
<TextCommand ExpectedResult="Table">
SELECT id, full_name, born_in FROM authors ORDER BY {if:byName{ full_name } else { id }}
</TextCommand>
</SqlOperation>
</OperationGroup>
</DataOperations>builder.Services.AddDataOperations(builder.Configuration);
builder.Services.AddDataSourceConnection(SqlProvider.Sqlite, cs => new SqliteConnection(cs));
builder.Services.AddOperationsDefinitionSource<AuthorOperationsSource>();// One operation: connect, address it, run it.
var authors = await dataOps.Connect().Query("Authors", "All")
.WithBinding("byName", true)
.ExecuteAsync<Author>();
// Several operations on one connection, in one transaction.
await using var scope = await dataOps.BeginAsync(AutoTransaction.Serializable);
await scope.Router.NonQuery("Authors", "Create").ExecuteAsync(author);
await scope.Router.NonQuery("Audit", "Append").ExecuteAsync(entry);
await scope.CommitAsync(); // disposing without this rolls backWhich engine an operation is written for is declared, not inferred: an operation compatible with
MySql PostgreSql is invisible to a SQLite connection, so the same key can carry a different
statement per engine.
Note. Column matching ignores underscores by default, so
full_namefillsFullName. This is Dapper's process-wide setting and it applies to settable properties only — a positionalrecordwill not materialize from underscored columns. Turn it off withDataOperations:MatchNamesWithUnderscores.
Full documentation at projectapricot.dev/docs/data-ops.