61

I am trying Dapper ORM and I am querying a a Posts table.

But I would like to get paged results ...

1 - How can I do this? Isn't there a helper for this?

2 - Can Dapper Query return an IQueryable?

Thank You, Miguel

Miguel Moura
  • 36,732
  • 85
  • 259
  • 481
  • 4
    Since you've just found out about Dapper... I hope you also heard about [PetaPoco](https://github.com/toptensoftware/PetaPoco) (with built-in paging support) and [Massive](https://github.com/robconery/massive)... Just FYI so you choose the one that suits you best... They're all extremely fast and very similar yet still different. – Robert Koritnik Mar 25 '12 at 19:56
  • see: http://samsaffron.com/archive/2011/09/05/Digging+ourselves+out+of+the+mess+Linq-2-SQL+created – Sam Saffron Apr 08 '12 at 08:35
  • No paging implementation is complete without the mention of being able to return TotalRecords or TotalPages. – Maulik Modi May 02 '21 at 11:29

9 Answers9

83

You didn't specify a database or version. If you're lucky enough to be able to use the brand new SQL Server 2012 and have access to MSDN, you can use the shiny new OFFSET and FETCH keywords. The following query will skip 20 records and return the next 5.

SELECT * FROM [Posts]
ORDER BY [InsertDate]
OFFSET 20 ROWS
FETCH NEXT 5 ROWS ONLY

Check out http://msdn.microsoft.com/en-us/library/ms188385(v=sql.110).aspx#Offset for more info.

Also, it's easy enough to copy the way Massive does it and write your own extension method for IDbConnection. Here's Massive's code.

var query = string.Format("SELECT {0} FROM (SELECT ROW_NUMBER() OVER (ORDER BY {2}) AS Row, {0} FROM {3} {4}) AS Paged ", columns, pageSize, orderBy, TableName, where);
Jarrett Meyer
  • 19,333
  • 6
  • 58
  • 52
  • Didn't know that SQL 2012 was already available. I though it was on RC ... Thanks – Miguel Moura Mar 29 '12 at 13:45
  • 8
    Personally, I use SqlBuilder for this stuff, turns out that non-trivial paging queries (especially ones involoving multi-mapping) require special care to get high perf see also: http://samsaffron.com/archive/2011/09/05/Digging+ourselves+out+of+the+mess+Linq-2-SQL+created – Sam Saffron Apr 08 '12 at 08:36
  • 9
    Parameterize that query!! – pim Sep 16 '17 at 00:06
  • 4
    Also don't dynamically include things like column names whenever possible. It's just asking for SQL injection. (I *definitely* wouldn't use string formatting like that in a random internet example for a highly visible question like this.) – jpmc26 Oct 17 '17 at 23:48
  • Anyone who uses this. Don't use string.Format on sql queries. Use dapper parameterized queries. If you have to use a dynamic set of parameters, use the builtin dynamic parameters. https://dapper-tutorial.net/parameter-dynamic – José Mancharo Oct 18 '21 at 23:12
46

1) Dapper doesn't have a built-in pagination feature. But its not too hard to implement it directly in the query. Example:

SELECT  *
FROM    ( SELECT    ROW_NUMBER() OVER ( ORDER BY InsertDate) AS RowNum, *
          FROM      Posts
          WHERE     InsertDate >= '1900-01-01'
        ) AS result
WHERE   RowNum >= 1 // *your pagination parameters
    AND RowNum < 20  //*
ORDER BY RowNum

Requires SQL Server 2005+

2) Dapper returns an IEnumerable<T>.

Alex
  • 7,901
  • 1
  • 41
  • 56
  • 5
    But if it returns an IEnumerable isn't this closed? I mean even if I change it to IQueryable the paging would be done in memory and not in SQL Server. So I think your approach is better ... – Miguel Moura Mar 26 '12 at 13:28
  • 3
    Yes, the IEnumerable is "closed" and doesn't have a link to the db context as you might know it from a IQueryable context with EF. – Alex Mar 26 '12 at 13:37
  • Dapper v1.13 does a .ToList() if you don't change the default buffer. Also the underlying code has already executing the query and is yielding the IDataReader.Read() so using Take and Skip won't work. So no, Dapper can not return an IQueryable. – BillRob Oct 28 '13 at 23:02
  • 1
    By "closed" he means "materialized" . – Skorunka František Nov 16 '16 at 16:26
  • Rownumber is easy solution but costly and not the best answer. – ddagsan Aug 22 '17 at 13:35
  • @ddagsan This can't be generalized - plus, OFFSET FETCH is not available until SQL Server 2012. If you want pagination with an older db engine than 2012, ROw_Number is the way to go. – Alex Aug 23 '17 at 07:45
7

Here is a full working version using C# and Dapper.

/// <summary>
    /// Gets All People
    /// </summary>
    /// <returns>List of People</returns>
    public IEnumerable<Person> GetAllPeople(Pager pager)
    {
        var sql = (@" select * from [dbo].[Person]
                      order by [PeplNo]
                      OFFSET      @Offset ROWS 
                      FETCH NEXT  @Next   ROWS ONLY");

        using (IDbConnection cn = Connection)
        {
            cn.Open();

            var results = cn.Query<Person>(sql,pager);

            return results;
        }
    }


public class Pager
{
    public int Page { get; set; }
    public int PageSize { get; set; }

    public int Offset { get; set; }
    public int Next { get; set; }

    public Pager(int page, int pageSize = 10)
    {
        Page = page < 1 ? 1 : page;
        PageSize = pageSize < 1 ? 10 : pageSize;

        Next = pageSize;
        Offset = (Page - 1) * Next;
    }

}
Kbdavis07
  • 1,012
  • 13
  • 24
  • 2
    This is a good example because it uses query parameterization. The top voted answer is vulnerable to SQL injection. https://owasp.org/www-community/attacks/SQL_Injection – José Mancharo Oct 18 '21 at 23:14
  • You misused variable `pageSize` in `Next = pageSize;` expression. It should be used `PageSize` (with capital P) instead. – Boris Šuška Dec 21 '22 at 23:19
2
public async Task<IEnumerable<Blog>> GetBlogs(int pageNo = 1, int pageSize = 10)
{
    int skip = (pageNo - 1) * pageSize;

    using (var db = _context.GetOpenConnection())
    {
       var query = string.Format(@"SELECT * FROM [blog] ORDER BY [Id] OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", skip, pageSize);
       
       var result = await db.QueryAsync<Blog>(query);

       return result;
    }
}
Rokive
  • 745
  • 9
  • 7
  • I see you stick to string.Format, you could try out string interpolation instead as IMHO this gives more readable code. Your sample however is very readable and can easily be adjusted to a generic extension method, I will use it as inspiration to write one. Thanks. Also your example was the cleanest I think, however OFFSET and FETCH do require at least SQL Server 2012 by reading from other posts here. – Tore Aurstad Jun 28 '21 at 23:17
  • Never use string interpolation or string.format in sql queries. Always parameterize in any function that can take user input. If you are dynamically assembling queries, do not take user input on what goes into the string. That leaves you vulnerable to SQL Injection https://owasp.org/www-community/attacks/SQL_Injection – José Mancharo Oct 18 '21 at 23:15
2

I created a generic method with strongly type arguments to get a reusable solution. This relies on FETCH NEXT and OFFSET, which means you need SQL Server 2012 or more recent.

    /// <summary>
    /// Fetches page with page number <paramref name="pageNumber"/> with a page size set to <paramref name="pageSize"/>.
    /// Last page may contains 0 - <paramref name="pageSize"/> items. The page number <paramref name="pageNumber"/> is 0-based,
    /// i.e starts with 0. The method relies on the 'FETCH NEXT' and 'OFFSET' methods
    /// of the database engine provider.
    /// Note: When sorting with <paramref name="sortAscending"/> set to false, you will at the first page get the last items.
    /// The parameter <paramref name="orderByMember"/> specified which property member to sort the collection by. Use a lambda.
    /// </summary>
    /// <typeparam name="T">The type of ienumerable to return and strong type to return upon</typeparam>
    /// <param name="connection">IDbConnection instance (e.g. SqlConnection)</param>
    /// <param name="orderByMember">The property to order with</param>
    /// <param name="sql">The select clause sql to use as basis for the complete paging</param>
    /// <param name="pageNumber">The page index to fetch. 0-based (Starts with 0)</param>
    /// <param name="pageSize">The page size. Must be a positive number</param>
    /// <param name="sortAscending">Which direction to sort. True means ascending, false means descending</param>
    /// <returns></returns>
    public static IEnumerable<T> GetPage<T>(this IDbConnection connection, Expression<Func<T, object>> orderByMember,
        string sql, int pageNumber, int pageSize, bool sortAscending = true)
    {
        if (string.IsNullOrEmpty(sql) || pageNumber < 0 || pageSize <= 0)
        {
            return null;
        }
        int skip = Math.Max(0, (pageNumber)) * pageSize;
        if (!sql.Contains("order by", StringComparison.CurrentCultureIgnoreCase))
        {
            string orderByMemberName = GetMemberName(orderByMember);
            sql += $" ORDER BY [{orderByMemberName}] {(sortAscending ? "ASC": " DESC")} OFFSET @Skip ROWS FETCH NEXT @Next ROWS ONLY";
            return connection.ParameterizedQuery<T>(sql, new Dictionary<string, object> { { "@Skip", skip }, { "@Next", pageSize } });
        }
        else
        {
            sql += $" OFFSET @Skip ROWS FETCH NEXT @Next ROWS ONLY";
            return connection.ParameterizedQuery<T>(sql, new Dictionary<string, object> { { "@Skip", skip }, { "@Next", pageSize } });
        }

    }

Supportive methods below, fetch property name and running parameterized query.

 private static string GetMemberName<T>(Expression<Func<T, object>> expression)
    {
        switch (expression.Body)
        {
            case MemberExpression m:
                return m.Member.Name;
            case UnaryExpression u when u.Operand is MemberExpression m:
                return m.Member.Name;
            default:
                throw new NotImplementedException(expression.GetType().ToString());
        }
    }

 public static IEnumerable<T> ParameterizedQuery<T>(this IDbConnection connection, string sql,
        Dictionary<string, object> parametersDictionary)
    {
        if (string.IsNullOrEmpty(sql))
        {
            return null;
        }
        string missingParameters = string.Empty;
        foreach (var item in parametersDictionary)
        {
            if (!sql.Contains(item.Key))
            {
                missingParameters += $"Missing parameter: {item.Key}";
            }
        }
        if (!string.IsNullOrEmpty(missingParameters))
        {
            throw new ArgumentException($"Parameterized query failed. {missingParameters}");
        }
        var parameters = new DynamicParameters(parametersDictionary);
        return connection.Query<T>(sql, parameters);
    }

Example usage using the Northwind DB:

        var sql = $"select * from products";
        var productPage = connection.GetPage<Product>(m => m.ProductID, sql, 0, 5, sortAscending: true);

Sample POCO look like this:

public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public int? SupplierID { get; set; }
    public int? CategoryID { get; set; }
    public string QuantityPerUnit { get; set; }
    public decimal? UnitPrice { get; set; }
    public short? UnitsInStock { get; set; }
    public short? UnitsOnOrder { get; set; }
    public short? ReorderLevel { get; set; }
    public bool? Discontinued { get; set; }
}

A more entensive variant of this approach could also build up a method to get an IEnumerable of IEnumerables using a DB Cursor and wrapping the logic used here, but my approach is a basic demonstration of a sturdy predictable type-safe solution, and not relying on the more flexible dynamic approach. The down-side is that since we have generic arguments, we must also write POCOs for our DB classes and not all developers like to spend time on that.

Tore Aurstad
  • 3,189
  • 1
  • 27
  • 22
1

I created a sample project to demo the Dapper custom paging, support sorting, criteria and filter:

https://github.com/jinweijie/Dapper.PagingSample

Basically, the method looks like this:

 Tuple<IEnumerable<Log>, int> Find(LogSearchCriteria criteria
        , int pageIndex
        , int pageSize
        , string[] asc
        , string[] desc);

The first return value is the item list. The second return value is the total count.

Hope it helps.

Thanks.

JIN Weijie
  • 11
  • 1
1

I find this solution to work for me. It also adds sorting.

public async Task<PagedCustomerResult> GetPagedCustomersAsync(string? sortBy = null, string? sortOrder = null, string pageNumber = 0, string pageSize = 0)
{
  var param = new
  { 
    offSet = (pageNumber -1) * pageSize, 
    pageSize 
  };

  // First query to get total number of records
  StringBuilder sb = new StringBuilder(@"SELECT COUNT(0) [Count] from Customers;");

  // Second query to get the data
  sb.AppendLine(@" SELECT * from Customers c");
  
  if (sortBy != null && sortOrder != null)
  {
    sb.AppendLine($" ORDER BY {BuildOrderBySqlUsingIntepolation(sortBy, sortOrder)}");
  }

  if (pageNumber > 0 && pageSize > 0)
  {
    sb.AppendLine(@" OFFSET @OffSet ROWS FETCH NEXT @PageSize ROWS ONLY");
  }
  
  var multi = await sqlConnection.QueryMultipleAsync(sb.ToString(), param);
  var totalRowCount = await multi.ReadSingleAsync<int>();
  var customers = await multi.ReadAsync<Customer>();

  return new PagedCustomerResult(customers, pageNumber, pageSize, totalRowCount);
  
} 

public class PagedCustomerResult
{
    private IEnumerable<Customer> _data;
    private int _currentPage;
    private int _pageSize;
    private int _totalRecords;
    private int _totalPages;
        
    public PagedCustomerResult(IEnumerable<Customer> data, int page, int pageSize, int totalRecords)
    {
        _data = data;
        _currentPage = page;
        _pageSize = pageSize;
        _totalRecords = totalRecords;
        _totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize);
    }

    public IEnumerable<Customer> Data => _data;

    public int CurrentPage => _currentPage;

    public int PageSize => _pageSize;

    public int TotalRecords => _totalRecords;

    public int TotalPages => _totalPages;
}

private static string BuildOrderBySqlUsingIntepolation(string sortOrderColumn, string sortOrderDirection)
{
   string orderBy;
   switch (sortOrderColumn)
   {
     case "name":
        orderBy = "c.[Name]";
        break;
     default:
        orderBy = "c.[CreatedDateTime]";
        break;
   }
   if (!string.IsNullOrEmpty(sortOrderDirection))
   {
      var sortOrder = "asc";
      if (sortOrderDirection == "desc")
      {
         sortOrder = "desc";
      }
      orderBy = $"{orderBy} {sortOrder}";
   }
   return orderBy;
}

Keshwar
  • 11
  • 2
0

If you want use pagination in dapper, You can use OFFSET and FETCH. I use of stored procedure. first type like this query in SQL and create your procedure:

CREATE PROCEDURE SpName    
    @OFFSET int
AS
BEGIN
    SELECT * 
    FROM TableName
    WHERE (if you have condition)
    ORDER BY columnName
        OFFSET @OFFSET ROWS  
        FETCH NEXT 25 (display 25 rows per page) ROWS ONLY  
END;  

Then you have create your method in your code for get data with Dapper and stored procedure :

public async Task<List<T>> getAllData<T>(string spName, DynamicParameters param)
{
    try
    {
        using (var con = new SqlConnection(_connections.DefaultConnection))
        {
            var result = await con.QueryAsync<T>(spName, param, commandType: System.Data.CommandType.StoredProcedure);
            return result.ToList();
        }
    }
    catch (Exception ex)
    {
        //
    }
}

Then eventually you call the stored procedure and set parameters (@OFFSET) in every method you want use dapper :

public async Task<List<YourModel>> methodName (int offset)
{
    var param = new DynamicParameters();
    param.Add("@OFFSET" , offset);

    var data = await getAllData<yourModel>("spName", param);
    var result =  _mapper.Map<List<yourModel>>(data);

    return result;
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
AlimItTech
  • 172
  • 1
  • 9
-15

If you do not have Sql Server 2012 or you have other DBMS, one way to do paging is to split the processing between the DBMS and the web server or client. ---this is recommended only for small set size. You can use the 'TOP' keyword in Sql Server or LIMIT in MySql or ROWNUM in Oracle to get the top number of rows in the data set. The number of rows that you would fetch is equals to the number that you would skip plus the number that you would take:

top = skip + take;

for instance, you would want to skip 100 rows and take the next 50:

top = 100 + 50

So your SQL statement would look like this (SQL server flavor)

SELECT    TOP 150 Name, Modified, content, Created
FROM      Posts
WHERE     Created >= '1900-01-01'

On the Client: if you are using a .NET language like C# and using Dapper, you can use linq to skip a number of rows and take a number of rows like so:

var posts = connection.Query<Post>(sqlStatement, dynamicParameters);
return posts?.ToList().Skip(skipValue).Take(takeValue);
  • 4
    I cannot recommend this solution because if you have many records in your database, you are transmitting all the records into memory and then you filter on your application side. You should filter and paginate data on DB side. – Miroslav Holec Jan 12 '17 at 16:03
  • 3
    The solution above clearly states this is can be used for a small set size. Paging by its own definition deals with a small set of records. – ErnestoDeLucia Jan 17 '17 at 04:27
  • 1
    But even though your paged data may be a small set of data, you are still loading back the whole set which may be huge in to memory. – James Hatton May 25 '17 at 08:24