Dapper.Paging.Demo
This sample demonstrate how to implement simple paging functionality in a ASP.NET Core Razor Pages application with Dapper and Microsoft SQL Server.
Install / Use
/learn @matjazbravc/Dapper.Paging.DemoREADME
Dapper.Paging.Demo
This simple demo application demonstrate how to implement simple paging functionality in a ASP.NET Core Razor Pages application with use of Dapper and Microsoft SQL Server.
Dealing with large datasets
Paging is a common technique that is used when dealing with large results sets. Typically, it is not useful for an application to request millions of records at a time because there is no efficient way to deal with all those records in memory all at once. This is especially true when rendering data on a grid in a user interface. User interface can only display a limited number of records at a time so it is generally a bad practice to hold everything in memory when only a small subset of records can be displayed at given time.
We’re continuing to use Dapper for our data access. It's lightweight "Micro ORM", ultra fast and it's in production use at Stack Overflow. You know what I mean. ;)
Let’s start!
For this sample we will use AdventureWork2016 OLTP database. The AdventureWorks databases are sample databases that were originally published by Microsoft to show how to design a SQL Server database using SQL Server. Download backup file from GitHub repository and restore it to the local SQL Server instance. All informations how to restore it you can find here or here.
Settings
We have to setup connection to SQL Server instance:
{
"SqlServerOptions": {
"SqlServerConnection": "Data Source=localhost;Initial Catalog=AdventureWorks2016;Trusted_Connection=True;"
},
"Serilog": {
"Using": [ "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}",
"restrictedToMinimumLevel": "Verbose"
}
},
{
"Name": "File",
"Args": {
"path": "./Log/DemoLog.txt",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{MemberName}] {Message}{NewLine}{Exception}",
"fileSizeLimitBytes": 1073741824,
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"restrictedToMinimumLevel": "Information",
"retainedFileCountLimit": "3",
"shared": true,
"flushToDiskInterval": "00:00:01"
}
}
],
"Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "Dapper.Paging.Demo"
}
},
"AllowedHosts": "*"
}
Create an Person Model/Entity
For this simple demo we will use just Persons model:
using System;
using System.ComponentModel.DataAnnotations;
namespace Dapper.Paging.Demo.Models
{
/// <summary>
/// Simplified Person model/entity
/// </summary>
[Serializable]
public class Person
{
[Key]
[Display(Name = "Person Id")]
public int BusinessEntityID { get; set; }
[Required]
[Display(Name = "Person Type")]
public string PersonType { get; set; }
[Display(Name = "Person Title")]
public string Title { get; set; }
[Required]
[Display(Name = "Person First name")]
public string FirstName { get; set; }
[Display(Name = "Person First name")]
public string MiddleName { get; set; }
[Required]
[Display(Name = "Person LAst name")]
public string LastName { get; set; }
[Display(Name = "Person Sufix")]
public string Suffix { get; set; }
[Display(Name = "Modified")]
public DateTime ModifiedDate { get; set; } = DateTime.UtcNow;
}
}
Create Person Repository
The next step is to create a Person Repository with single method GetAsync which reads paged items from [Person].[Person] table:
using Dapper.Paging.Demo.Configuration;
using Dapper.Paging.Demo.Models;
using Dapper.Paging.Demo.Services;
using Microsoft.Extensions.Options;
using System.Data.SqlClient;
using System.Threading.Tasks;
namespace Dapper.Razor.Demo.Services.Repositories
{
/// <summary>
/// Persons repository
/// </summary>
public class PersonRepository : IPersonRepository
{
private readonly SqlServerOptions _sqlServerOptions;
public PersonRepository(IOptions<SqlServerOptions> sqlServerOptions)
{
_sqlServerOptions = sqlServerOptions.Value;
}
public async Task<PagedResults<Person>> GetAsync(string searchString = "", int pageNumber = 1, int pageSize = 10)
{
using (var conn = new SqlConnection(_sqlServerOptions.SqlServerConnection))
{
await conn.OpenAsync();
// Set first query
var whereStatement = string.IsNullOrWhiteSpace(searchString) ? "" : $"WHERE [FirstName] LIKE '{searchString}'";
var queries = @$"
SELECT
[BusinessEntityID],
[PersonType],
[Title],
[FirstName],
[MiddleName],
[LastName],
[Suffix],
[ModifiedDate] FROM [Person].[Person] (NOLOCK)
{whereStatement}
ORDER BY [BusinessEntityID]
OFFSET @PageSize * (@PageNumber - 1) ROWS
FETCH NEXT @PageSize ROWS ONLY;";
// Set second query, separated with semi-colon
queries += "SELECT COUNT(*) AS TotalItems FROM [Person].[Person] (NOLOCK);";
// Execute multiple queries with Dapper in just one step
using var multi = await conn.QueryMultipleAsync(queries,
new
{
PageNumber = pageNumber,
PageSize = pageSize
});
// Fetch Items by OFFSET-FETCH
var items = await multi.ReadAsync<Person>().ConfigureAwait(false);
// Fetch Total items count
var totalItems = await multi.ReadFirstAsync<int>().ConfigureAwait(false);
// Create paged result
var result = new PagedResults<Person>(totalItems, pageNumber, pageSize)
{
Items = items
};
return result;
}
}
}
}
The key for retrieving paged items we are using SQL OFFSET-FETCH Clause which were introduced with SQL Server 2012 and which are great for building pagination support. Some additional informations about SQL OFFSET-FETCH Clause you can find here.
According to input parameters we are returning Paged results with this helper class:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dapper.Paging.Demo.Services
{
/// <summary>
/// Helper class for returning paged results
/// </summary>
/// <typeparam name="T"></typeparam>
public class PagedResults<T>
{
public PagedResults(int totalItems,
int pageNumber = 1,
int pageSize = 10,
int maxNavigationPages = 5)
{
// Calculate total pages
var totalPages = (int)Math.Ceiling(totalItems / (decimal)pageSize);
// Ensure actual page isn't out of range
if (pageNumber < 1)
{
pageNumber = 1;
}
else if (pageNumber > totalPages)
{
pageNumber = totalPages;
}
int startPage;
int endPage;
if (totalPages <= maxNavigationPages)
{
startPage = 1;
endPage = totalPages;
}
else
{
var maxPagesBeforeActualPage = (int)Math.Floor(maxNavigationPages / (decimal)2);
var maxPagesAfterActualPage = (int)Math.Ceiling(maxNavigationPages / (decimal)2) - 1;
if (pageNumber <= maxPagesBeforeActualPage)
{
// Page at the start
startPage = 1;
endPage = maxNavigationPages;
}
else if (pageNumber + maxPagesAfterActualPage >= totalPages)
{
// Page at the end
startPage = totalPages - maxNavigationPages + 1;
endPage = totalPages;
}
else
{
// Page in the middle
startPage = pageNumber - maxPagesBeforeActualPage;
endPage = pageNumber + maxPagesAfterActualPage;
}
}
// Create list of Page numbers
var pageNumbers = Enumerable.Range(startPage, (endPage + 1) - startPage);
StartPage = startPage;
EndPage = endPage;
PageNumber = pageNumber;
PageNumbers = pageNumbers;
PageSize = pageSize;
TotalItems = totalItems;
TotalPages = totalPages;
}
public IEnumerable<T> Items { get; set; }
/// <summary>
/// Total number of items to be paged
/// </summary>
public int TotalItems { get; set; }
/// <su
