-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
Everything here targets .NET 10. Packages are on NuGet at version 2.0.0.
You never install the core package directly — every provider brings it along.
# Parameterized SQL over Dapper: SQL Server, PostgreSQL, MySQL/MariaDB, Oracle, SQLite
dotnet add package PepperX.QueryForge.Dapper
# Entity Framework Core generates the SQL from your model
dotnet add package PepperX.QueryForge.EFCore
# Any IEnumerable<T> — cached data, composed results, tests
dotnet add package PepperX.QueryForge.InMemoryInstalling more than one is normal and supported. The sample application uses all three against the same data so their answers can be compared side by side.
The Dapper provider does not reference any database driver. Bring the one you use:
dotnet add package Microsoft.Data.SqlClient # SQL Server
dotnet add package Npgsql # PostgreSQL
dotnet add package MySqlConnector # MySQL / MariaDB
dotnet add package Oracle.ManagedDataAccess.Core # Oracle
dotnet add package Microsoft.Data.Sqlite # SQLitepublic class User
{
public int UserId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public string? Department { get; set; }
public decimal Score { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedOn { get; set; }
}Column names in a query are matched to property names, case-insensitively. A model whose properties differ from the column names your callers use is handled by a custom value accessor or by mapping in your own code.
The shortest path to a working query — no database, no registration.
using PepperX.QueryForge;
using PepperX.QueryForge.InMemory;
var users = LoadUsersFromSomewhere(); // IEnumerable<User>
var query = QueryBuilder.New()
.Sort(new SortDescriptor("Score", SortOrder.Descending))
.Page(size: 10, number: 1)
.Build();
QueryResult<User> result = users.ToQueryResult(query);
Console.WriteLine(result.Meta.Total.Rows); // total matching rows, before paging
Console.WriteLine(result.Meta.Total.Pages); // ceil(rows / size)
Console.WriteLine(result.Models.Count); // up to 10No registration either — these are extension methods on IQueryable<T>.
using PepperX.QueryForge;
using PepperX.QueryForge.EFCore;
app.MapPost("/api/users/query", async (Query query, AppDbContext db) =>
await db.Users.AsNoTracking().ToQueryResultAsync<User>(query));Because they compose with an existing IQueryable, a restriction you apply first survives — which is
the pattern for tenant isolation:
await db.Users
.Where(u => u.TenantId == currentTenant) // the client cannot query this away
.AsNoTracking()
.ToQueryResultAsync<User>(query);This one needs a one-line registration at startup, because the provider keeps a per-engine schema cache and needs somewhere to live.
using PepperX.QueryForge.Dapper;
builder.Services.AddQueryForgeDapper(options =>
{
// Only needed for the overload that manages its own connection.
options.ConnectionFactory = sp =>
new SqlConnection(sp.GetRequiredService<IConfiguration>().GetConnectionString("Default"));
});All five dialects are registered for you. Then:
app.MapPost("/api/users/query", async (Query clientQuery, IDapperQueryService svc) =>
{
var dapperQuery = DapperQueryBuilder
.FromBase(clientQuery) // filters/paging/sorting from the client
.ForObject("Users", "dbo", DapperObjectType.Table) // the target is set server-side
.Build();
return await svc.QueryAsync<User>(dapperQuery);
});The client never names the table. A
Queryhas no object property at all — onlyDapperQuerydoes, and you set it on the server. That is a structural guarantee, not a convention.
using PepperX.QueryForge.Dapper;
app.MapPost("/api/users/query", async (Query clientQuery, IDbConnection connection) =>
{
var dapperQuery = DapperQueryBuilder.FromBase(clientQuery).ForObject("Users", "dbo").Build();
return await connection.QueryForgeAsync<User>(dapperQuery);
});QueryForgeAsync works even in an application that never called AddQueryForgeDapper() — the
package keeps a default registry carrying all five built-in dialects. Calling AddQueryForgeDapper()
replaces it, which is how a custom dialect reaches the extension method too.
The Dapper provider infers the engine from the connection type name you hand it. One registration serves an application talking to several databases at once:
await svc.QueryAsync<User>(new NpgsqlConnection(pg), query); // PostgreSQL syntax
await svc.QueryAsync<User>(new SqlConnection(mssql), query); // SQL Server syntax
await svc.QueryAsync<User>(new OracleConnection(oracle), query); // Oracle syntax| Connection type name | Engine |
|---|---|
SqlConnection |
SQL Server |
NpgsqlConnection |
PostgreSQL |
MySqlConnection, MariaDbConnection
|
MySQL / MariaDB |
OracleConnection |
Oracle |
SqliteConnection |
SQLite |
Anything else throws NotSupportedException. See Extending QueryForge to add
your own.
A Query that arrived from outside your API should never be executed as-is. See
Validation for the full rule set.
query.Validate(rules =>
{
rules.Select(c => c.Deny("PasswordHash", "SecretKey"));
rules.Where(c => c.Deny("PasswordHash", "SecretKey"));
rules.Sort(c => c.Allow("Score", "CreatedOn", "LastName"));
rules.PageSize(p => p.Max(100));
}, QueryValidationMode.SilentStrip);SilentStrip removes what is not allowed and lets the request succeed. ThrowException raises
QueryValidationException listing every violation. Public APIs usually want the first; internal ones
usually want the second.
- Query Model — every field of a query, and what it does
- Query Semantics — the precise evaluation rules, including nulls and negation
- JSON Contract — the wire format, with every enum's numeric value
- Grouping and Hierarchies — turning a flat table into a nested tree
- Recipes — data grids, tenant isolation, exports, search boxes
QueryForge · part of the PepperX Ecosystem · MIT licensed · packages 2.0.0, .NET 10
Foundations
- Getting Started
- Architecture
- Query Model
- Query Semantics
- Results and Metadata
- JSON Contract
- Fluent Builders
Behaviour
Providers
- Dapper Provider
- Dapper: Generated SQL
- Dapper: Dialects
- EF Core Provider
- EF Core: Joins & Includes
- In-Memory Provider
Practice
Reference