Extending — dialects and the SPI
Writing a relational dialect
The cheapest way to support another SQL engine is a SqlDialect subclass — the shared engine does
the rest. The dialect answers only the questions engines disagree on:
public sealed class DuckDbDialect : SqlDialect
{
public override string Quote(string identifier) => $"\"{identifier}\"";
public override string ColumnName(string memberName) => ToSnakeCase(memberName);
public override string TableName(string typeName) => ToSnakeCase(typeName);
public override string SqlType(Type type) => /* CLR → DDL type map */;
// paging syntax, generated-key DDL + retrieval, parameter configuration,
// index structures (CreateIndexSql / SearchIndexSql), sized types (SizedTextType)…
// every member has a base implementation or a documented reason to override.
}
Register it the way the shipped providers do (their Extensions/ folders are the template): a
*Database extension building the RelationalModelBuilder with your dialect, plus the shared
AddRelationalRepositories/AddRelationalMigrations wiring. The MariaDB dialect inside the MySql
package is a complete worked example of "same engine, different truths" — it overrides generated-key
retrieval and nothing else.
The dialect contract is honesty: for a structure your engine lacks (filtered indexes, GIN),
throw NotSupportedException with guidance — the base class shows the message style. Never emit
approximate DDL.
Custom unit of work
Every provider's Add*Repositories<TUnitOfWork>() overload accepts your subclass of the provider's
unit of work — the seam for cross-cutting write behavior (outbox staging, domain-event dispatch on
commit) without giving up the engine:
public class ShopUnitOfWork(IServiceProvider sp, /* provider deps */) : PostgreSqlUnitOfWork(sp, …)
{
// override commit hooks, expose typed repositories, etc.
}
services.AddPostgreSqlRepositories<ShopUnitOfWork>();
Custom repositories
Subclass the provider repository to add domain query methods while inheriting the whole engine surface:
public sealed class OrderRepository(IQueryableUnitOfWork uow)
: RelationalRepository<Order, Guid>(uow), IOrderRepository
{
public Task<IEnumerable<Order>> OverdueAsync(DateTime today) =>
GetFilteredAsync(o => o.DueDate < today && o.Status == OrderStatus.Open);
}
The SPI boundary
eQuantic.Core.Data.Query (the interpreters and IR) is public for providers, hidden from
IntelliSense ([EditorBrowsable(Never)]), and versioned as SPI: if you build a full provider on
it, pin minor versions and read release notes — the repository contracts are the stable API,
the IR is the extensible engine seam. A new provider implements: a model builder with the
annotation pre-pass (the pattern is identical in all four shipped ones), renderers from the IR to
the store's query/write forms, a unit of work with staged writes, and the repository classes over
it. The Cassandra provider is the best reference implementation — it exercises every honesty
mechanism the engine has.