I am working with C#, Npgsql, EF Core and Postgres.
I defined an endpoint for a paginated search, where the filters and orderBy column are dynamic. The endpoint accepts a PaginationOptions instance:
public class PaginationOptions
{
public int Page { get; set; }
public int ItemsPerPage { get; set; }
public string OrderBy { get; set; }
public bool Desc { get; set; }
public IList<FilterValues> Filters { get; set; }
}
public class FilterValues
{
public string FieldName { get; set; }
public IEnumerable<string> Values { get; set; }
}
The following method performs the search and returns a Tuple with the sorted items and a counter for the total items in the table:
public Tuple<IList<T>, int> Search(PaginationOptions paginationOptions)
{
if (!string.IsNullOrEmpty(paginationOptions.OrderBy))
{
CheckFilterField(paginationOptions.OrderBy);
}
int offset = (paginationOptions.Page - 1) * paginationOptions.ItemsPerPage;
string orderBy = !string.IsNullOrEmpty(paginationOptions.OrderBy) ? paginationOptions.OrderBy : $"{prefix}.title";
string order = paginationOptions.Desc ? "DESC" : "ASC";
using (NpgsqlConnection connection = GetConnection())
{
string query = $"{GetQueryfields()} {GetFromClause()} {BuildWhere(paginationOptions.Filters)}";
string itemsQuery = $"SELECT {query} ORDER BY {orderBy} {order}";
NpgsqlCommand command = BuildCommand(connection, itemsQuery, paginationOptions.Filters);
IDataReader reader = command.ExecuteReader();
ISet<Guid> guids = new HashSet<Guid>(paginationOptions.ItemsPerPage);
while (reader.Read())
{
Guid guid = reader.GetGuid(0);
if (!guids.Contains(guid))
{
guids.Add(guid);
}
}
ISet<Guid> filteredGuids = guids.Skip(offset).Take(paginationOptions.ItemsPerPage).ToHashSet();
IList<T> items = GetItems(filteredGuids);
return Tuple.Create(items, guids.Count);
}
}
In words: In each entity there are the query fields and the FROM clause defiend. They are splitted because I need the FROM clause in another method as well.
The WHERE (prepared statement) and ORDER BY are built dynamically using the parameters. The BuildCommand creates the NpgsqlCommand and sets the parameters. Then I use Dapper for a raw query in order to get the ids of the requested items, then I load them using the EF and at the end I Skip and Take in order to have the right pagination.
The problem ist that EF does not allow to add an ORDER BY clause for raw queries, it is only available throug the Linq expression:
context.AnEntity.FromSqlRaw("Select * from users ORDER BY id").OrderBy(x => x.Title);
ORDER BY id is ignored, items are sorted by the expression. If no orderby linq expression is used, the framework adds ORDER BY entity.id. Otherwise I could have done followings:
string itemsQuery = $"SELECT {query} ORDER BY {orderBy} {order}";
context.AnEntity.FromSqlRaw(itemsQuery).Skip(offset).Take(limit)...
It works. Even on a table with 1mil a query takes 2,8sec
Comments? Improvment hints?
Edit:
I ended up with a query which loads the paged data in 2,2sec over a table with 1mil rows. Is it an acceptable result?
id (uuid)column is the primary key. \$\endgroup\$DynamicLinq, will take a look. I am not fan ofODatabut I will give it a try. \$\endgroup\$