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 ofOData
but I will give it a try. \$\endgroup\$