Skip to content

Getting Started

AmirHosseinMp02 edited this page Aug 1, 2026 · 1 revision

Getting Started

Everything here targets .NET 10. Packages are on NuGet at version 2.0.0.


1. Pick a provider

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.InMemory

Installing 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         # SQLite

2. The model in these examples

public 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.


3. First query: In-Memory

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 10

4. First query: EF Core

No 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);

5. First query: Dapper

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 Query has no object property at all — only DapperQuery does, and you set it on the server. That is a structural guarantee, not a convention.

Without a service dependency

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.


6. Which engine gets used

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.


7. Add safety before you ship

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.


8. Where to go next

Clone this wiki locally