This document shows how to implement custom paging strategies in LiteOrm, using Oracle 11g as an example.
When the target database version is old, or the default dialect cannot satisfy the current SQL specification, you need custom paging logic. The most typical scenarios are:
Below is the complete implementation of Oracle11gBuilder, which inherits from OracleBuilder and overrides the paging logic:
public class Oracle11gBuilder : OracleBuilder
{
/// <summary>
/// Gets the singleton instance of <see cref="Oracle11gBuilder"/>, suitable for Oracle 11g and above.
/// </summary>
public readonly static new Oracle11gBuilder Instance = new Oracle11gBuilder();
/// <summary>
/// Assembles the structured SQL fragment into the final SELECT statement (Oracle implementation).
/// Uses ROW_NUMBER() OVER(...) double-layered nested subquery for paging, compatible with all Oracle versions.
/// </summary>
public override void BuildSelectSql(ref SqlValueStringBuilder subSelect, ref ValueStringBuilder result)
{
bool hasPaging = subSelect.Take > 0;
if (hasPaging)
{
// Outer: filter ROW_NUMBER() range
result.Append("SELECT * FROM (\n");
}
// Inner: actual data query
result.Append("SELECT ");
result.Append(subSelect.Select.AsSpan());
if (hasPaging)
{
// Inner: calculate ROW_NUMBER(), move ORDER BY to OVER clause
result.Append(",ROW_NUMBER() OVER (ORDER BY ");
if (subSelect.OrderBy.Length > 0)
result.Append(subSelect.OrderBy.AsSpan());
else
result.Append('1');
result.Append(") AS \"RN__\"");
}
if (subSelect.From.Length > 0)
{
result.Append(" \nFROM ");
result.Append(subSelect.From.AsSpan());
}
if (subSelect.Where.Length > 0)
{
result.Append(" \nWHERE ");
result.Append(subSelect.Where.AsSpan());
}
if (subSelect.GroupBy.Length > 0)
{
result.Append(" \nGROUP BY ");
result.Append(subSelect.GroupBy.AsSpan());
}
if (subSelect.Having.Length > 0)
{
result.Append(" \nHAVING ");
result.Append(subSelect.Having.AsSpan());
}
if (hasPaging)
{
// Close inner subquery, provide alias for outer layer reference
result.Append("\n) \"__T\"\n");
// Filter by ROW_NUMBER() range (1-based, skip items after, take total items)
result.Append("WHERE \"RN__\" > ");
result.Append(subSelect.Skip.ToString());
result.Append(" AND \"RN__\" <= ");
result.Append((subSelect.Skip + subSelect.Take).ToString());
}
else
{
if (subSelect.OrderBy.Length > 0)
{
result.Append(" \nORDER BY ");
result.Append(subSelect.OrderBy.AsSpan());
}
}
}
}
Oracle 11g does not support LIMIT and OFFSET syntax, so ROW_NUMBER() OVER() function must be used to implement paging:
RN__ for each rowROW_NUMBER() OVER(ORDER BY ...) AS RN__ in the inner queryRN__ fieldThere are three ways to register a custom Oracle11gBuilder:
// Register at application startup
using Oracle.ManagedDataAccess.Client;
// Register by connection type
SqlBuilderFactory.Instance.RegisterSqlBuilder(typeof(OracleConnection), Oracle11gBuilder.Instance);
// Or register by data source name
SqlBuilderFactory.Instance.RegisterSqlBuilder("OracleDataSource", Oracle11gBuilder.Instance);
// Specify custom SqlBuilder when registering LiteOrm
using Oracle.ManagedDataAccess.Client;
using System.Reflection;
var host = Host.CreateDefaultBuilder(args)
.RegisterLiteOrm(options =>
{
// Register by data source name
options.RegisterSqlBuilder("OracleDataSource", Oracle11gBuilder.Instance);
// Or register by connection type (global replacement)
options.RegisterSqlBuilder(typeof(OracleConnection), Oracle11gBuilder.Instance);
})
.Build();
Specify the custom SqlBuilder type name directly via the SqlBuilder field in appsettings.json:
{
"LiteOrm": {
"Default": "OracleDataSource",
"DataSources": [
{
"Name": "OracleDataSource",
"ConnectionString": "Data Source=ORCL;User Id=user;Password=pass;",
"Provider": "Oracle.ManagedDataAccess.Client.OracleConnection, Oracle.ManagedDataAccess",
"SqlBuilder": "YourNamespace.Oracle11gBuilder, YourAssembly",
"PoolSize": 20,
"MaxPoolSize": 100
}
]
}
}
Note:
SqlBuilder field is FullTypeName, AssemblyNameusing static LiteOrm.Common.Expr;
// Using service layer
var pageResult = await userService.SearchAsync(
q => q.Where(u => u.Age >= 18)
.OrderBy(u => u.Id)
.Skip(10).Take(20)
);
// Using DAO directly
var users = await objectViewDAO.Search(
From<User>()
.Where(u => u.Age >= 18)
.OrderBy(nameof(User.Id))
.Section(10, 20) // Skip 10 items, take 20 items
).ToListAsync();
using static LiteOrm.Common.Expr;
var query = From<User>()
.Where(Prop("Age") > 18 & Prop("DeptId").In(1, 2, 3))
.OrderByDescending("CreateTime")
.Section(0, 10); // First page, 10 records
var result = await userService.SearchAsync(query);
// 1. Define custom Builder
public class Oracle11gBuilder : OracleBuilder
{
public static readonly Oracle11gBuilder Instance = new Oracle11gBuilder();
}
// 2. Register to LiteOrm
builder.Host.RegisterLiteOrm(options =>
{
options.RegisterSqlBuilder("OracleDataSource", Oracle11gBuilder.Instance);
});
// 3. Use paging API normally, no need to rewrite queries in business code
var page = await userService.SearchAsync(
q => q.Where(u => u.Age >= 18)
.OrderBy(u => u.Id)
.Skip(20)
.Take(20)
);
The key point of this pattern is: paging differences are only handled in SqlBuilder, while the business layer maintains a unified Skip/Take写法.
SELECT "T0"."ID", "T0"."USERNAME", "T0"."AGE", "T0"."CREATETIME"
FROM "USERS" "T0"
WHERE "T0"."AGE" >= :0
ORDER BY "T0"."ID"
SELECT * FROM (
SELECT "T0"."ID", "T0"."USERNAME", "T0"."AGE", "T0"."CREATETIME",ROW_NUMBER() OVER (ORDER BY "T0"."ID") AS "RN__"
FROM "USERS" "T0"
WHERE "T0"."AGE" >= :0
) "__T"
WHERE "__T"."RN__" > 10 AND "__T"."RN__" <= 30
Note:
users, id)You can refer to the Oracle11gBuilder implementation to create custom paging strategies for other databases:
public class SqlServer2008Builder : SqlServerBuilder
{
public readonly static new SqlServer2008Builder Instance = new SqlServer2008Builder();
public override void BuildSelectSql(ref SqlValueStringBuilder subSelect, ref ValueStringBuilder result)
{
// Implement TOP + ROW_NUMBER() paging
// ...
}
}
public class CustomPostgreSqlBuilder : PostgreSqlBuilder
{
public readonly static new CustomPostgreSqlBuilder Instance = new CustomPostgreSqlBuilder();
public override void BuildSelectSql(ref SqlValueStringBuilder subSelect, ref ValueStringBuilder result)
{
// Implement custom paging logic
// ...
}
}
Problem: Large data volume paging queries are slow
Solution:
Problem: Paged results sort incorrectly
Solution:
ORDER BY 1 is used as the default sortBy implementing a custom SqlBuilder, you can provide optimal paging strategies for different database versions and scenarios, thereby improving query performance and compatibility. LiteOrm’s modular design makes this extension very simple and intuitive.