When using Linq I am not able to make use of SQL Full-Text Search capability so I would like to use SqlQuery method on DbSet which allows me to write a raw SQL query.
Let consider following code:
IQueryable<Message> query = DBContext.Set<Message>();
PagedList<Message> messages = query.Where(x => x.Content.Contains("magic"))
.OrderBy(x => x.CreatedDate)
.ToPagedList<Message>(50, 2);
if I would like to run following SqlQuery query:
DBContext.Set<Message>().SqlQuery("SELECT * FROM Message WHERE CONTAINS("Content", 'magic')");
SqlQuery method returns IEnumerable<Message> and I would like to have PagedList<Message> object returned. What would be the optimal(good) way to achieve it? Notice that I would like my database to execute SQL query and not to filter in memory.
PagedList implementation
public class PagedList<T> : List<T>, IPagedList
{
public PagedList()
{
this.TotalCount = 0;
this.PageSize = 0;
this.PageIndex = 0;
this.TotalPages = 0;
this.CurrentPage = 0;
}
public PagedList(IQueryable<T> source, int pageIndex, int pageSize)
{
this.TotalCount = source.Count();
this.PageSize = pageSize;
this.PageIndex = pageIndex;
this.TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
this.CurrentPage = Math.Max(1, pageIndex);
this.AddRange(source.Skip((this.CurrentPage - 1) * pageSize).Take(pageSize).ToList());
}
public PagedList(List<T> source, int index, int pageSize)
{
this.TotalCount = source.Count();
this.PageSize = pageSize;
this.PageIndex = index;
this.TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
this.CurrentPage = Math.Max(1, index);
this.AddRange(source.Skip(index * pageSize).Take(pageSize).ToList());
}
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public int PageSize { get; set; }
public int PageIndex { get; set; }
public int CurrentPage { get; set; }
}
Paged list extension method:
public static PagedList<T> ToPagedList<T>(this IQueryable<T> query, int pageIndex, int pageSize) where T : class
{
return new PagedList<T>(query, pageIndex, pageSize);
}
Have you tried to simply do
DBContext.Set<Message>().SqlQuery("SELECT * FROM Message WHERE CONTAINS("Content", 'magic')")
.AsQueryable()
.ToPagedList<Message>(50, 2);
Related
I'm having difficulties getting all members mapped in the following scenario:
I have a class that inherits from List<T>:
public class PagedList<T> : List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
AddRange(items);
}
public PagedList()
{
//default constructor added because Mapster complained about missing default constructor
}
public static async Task<PagedList<T>> ToPagedListAsync(IQueryable<T> source, int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
var count = source.Count();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
Implementation:
public async Task<PagedList<UserDTO>> GetAllAsync(UserParameters userParameters, CancellationToken cancellationToken = default)
{
PagedList<User> users = await _repositoryManager.UserRepository.GetAllAsync(userParameters, cancellationToken);
//users.Count = 5
//users.TotalPages = 10
TypeAdapterConfig<PagedList<User>, PagedList<UserDTO>>.NewConfig()
.IncludeMember((member, side) => member.AccessModifier == AccessModifier.Internal ||
member.AccessModifier == AccessModifier.ProtectedInternal);
PagedList<UserDTO> usersDTO = users.Adapt<PagedList<UserDTO>>();
//usersDTO.Count = 5
//BUT usersDTO.TotalPages = 0 (should be 10)
return usersDTO;
}
In the above scenario, the items of the inherited List<User> list in PagedList<User> are converted properly, whereas the other members, i.e. CurrentPage, TotalPages, PageSize, TotalCount, HasPrevious and HasNext are not.
I tried to configure Mapster to include also hidden members, but to no avail:
TypeAdapterConfig<PagedList<User>, PagedList<UserDTO>>.NewConfig()
.IncludeMember((member, side) => member.AccessModifier == AccessModifier.Internal ||
member.AccessModifier == AccessModifier.ProtectedInternal);
How do I go about to make this conversion work?
I was facing similar issue and didn't find any clear way, so I made a workaround by adding a constructor with count, pageNumber and pageSize properties.
Then I configured to use this constructor by calling ConstructUsing.
TypeAdapterConfig<PagedList<User>, PagedList<UserDTO>>.NewConfig()
.ConstructUsing(src =>
new PagedList<UserDTO>(src.TotalCount, src.PageSize, src.CurrentPage));
public class PagedList<T> : List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagedList(List<T> items, int count, int pageNumber, int pageSize) : this(count, pageNumber, pageSize)
{
AddRange(items);
}
public PagedList(int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
}
public static async Task<PagedList<T>> ToPagedListAsync(IQueryable<T> source, int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
var count = source.Count();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
I have finally found a “solution” to this problem by changing my approach.
I changed my service implementation to:
public async Task<(object, List<UserDTO>)> GetAllAsync(UserParameters userParameters, CancellationToken cancellationToken = default)
{
PagedList<User> users = await _repositoryManager.UserRepository.GetAllAsync(userParameters, cancellationToken);
var metadata = new
{
users.TotalCount,
users.PageSize,
users.CurrentPage,
users.TotalPages,
users.HasNext,
users.HasPrevious
};
var usersDTO = users.Adapt<List<UserDTO>>();
var retVal = (metadata, usersDTO);
return retVal;
}
Then in the receiving controller method, I serialise metadata and adds it to the header and returning the list.
Like this:
public async Task<IActionResult> GetUsers([FromQuery] UserParameters userParameters, CancellationToken cancellationToken)
{
var tuple = await _serviceManager.UserService.GetAllAsync(userParameters, cancellationToken);
var metadata = tuple.Item1;
var usersDTO = tuple.Item2;
Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(metadata));
var links = _userLinks.TryGenerateLinks(usersDTO, userParameters.Fields, HttpContext);
return links.HasLinks ? Ok(links.LinkedEntities) : Ok(links.ShapedEntities);
}
Thanks to Marinko Spasojevic at Code-Maze for pointing me in the right direction.
I want to return the current pages and total pages form a generic method.
The implementation of the generic function is done as:
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; set; }
public int TotalPages { get; set; }
public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}
public bool HasNextPage
{
get
{
return (PageIndex < TotalPages);
}
}
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
However, this function does not return the PageIndex and TotalPages to the calling function.
It always returns the items list.
I am calling the method as:
public async Task<IActionResult> UserActivityList(string currentFilter,
string searchString, int? pageIndex, int pageSize = 10)
{
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
string CurrentFilter = searchString;
IQueryable<UserActivityList> results = _logContext.UserActivityLists;
if (!string.IsNullOrEmpty(searchString))
{
results = results.Where(s => s.Name.Contains(searchString));
}
var UserActivityLists = await PaginatedList<UserActivityList>.CreateAsync(results.AsNoTracking(), pageIndex ?? 1, pageSize);
return Ok(UserActivityLists);
}
The value of UserActivityList, I have formatted the output to JSON, while am calling through a PostMan.
[
{
"id": 2186,
"name": "abc cbc"
},
{
"id": 2152,
"name": "def efg"
},
{
"id": 2238,
"name": "kgh tbf"
},
{
"id": 2172,
"name": "loc tbh"
},
{
"id": 1651,
"name": "abc abc"
}
]
What is the reason the PaginatedList not returning other property? What is the thing I am missing here?
This is due to the serializer just seeing an IEnumerable<T> and doing it's (default) thing. Can I suggest that your class does not inherit from List<T> as it should not have an is-a relationship - it should be a has-a relationship which means your PaginatedList should expose a List<T> (or IEnumerable<T>) property.
With this change you can more exactly control how serialization of your object is handled.
public class PaginatedList<T>
{
public int PageIndex { get; set; }
public int TotalPages { get; set; }
public List<T> Items { get; set; }
public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
Items = items;
}
public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}
public bool HasNextPage
{
get
{
return (PageIndex < TotalPages);
}
}
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
There is this good question/answer about inheriting from List<T>: Why not inherit from List<T>?
A very simple solution is of course to refactor your code so that you do not inherit form List, but expose a List as a property:
public class Document<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set;}
public List<T> Pages {get; private set; }
public Document(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.Pages = new List<T>(items);
}
// ...
}
Is there a way to implement Generic filter for NHibernate on a repository layer? As per Microsoft API Guidelines, the sort and filters are passed as string API Guidelines. what will be the approach to that using NHibernate to make it generic?
Please have a look at this post.There is a section Paging with NHibernate.
The code snippet below is from this post.
The approach is to implement a class (in the post it was PagedResult<T>) in your case FilteredResult<T>. Having this class you can implementan extension (in post was NHibernatePagedResultExtensions) in this case NHibernateFilteredResultExtensions.
And the last thing left is to apply this extansion exacly the same way.
Hope this will help.
public abstract class PagedResultBase
{
public int CurrentPage { get; set; }
public int PageCount { get; set; }
public int PageSize { get; set; }
public int RowCount { get; set; }
public string LinkTemplate { get; set; }
public int FirstRowOnPage
{
get { return (CurrentPage - 1) * PageSize + 1; }
}
public int LastRowOnPage
{
get { return Math.Min(CurrentPage * PageSize, RowCount); }
}
}
public class PagedResult<T> : PagedResultBase
{
public IList<T> Results { get; set; }
public PagedResult()
{
Results = new List<T>();
}
}
public static class NHibernatePagedResultExtensions
{
public static async Task<PagedResult<T>> GetPagedAsync<T>(this IQueryable<T> query, int page, int pageSize)
{
var result = new PagedResult<T>
{
CurrentPage = page,
PageSize = pageSize,
RowCount = query.Count()
};
var pageCount = (double)result.RowCount / pageSize;
result.PageCount = (int)Math.Ceiling(pageCount);
var skip = (page - 1) * pageSize;
result.Results = await query.Skip(skip).Take(pageSize).ToListAsync();
return result;
}
}
public async Task<IActionResult> Index(int page = 1)
{
var result = await _session.RunInTransaction(async () =>
{
var books = await _session.Books
.Where(b => b.Title.StartsWith("How to"))
.GetPagedAsync(page, pageSize: 25);
return _mapper.Map<PagedResult<BookModel>>(books);
});
return View(result);
}
I used the paging example of the Nerddinner tutorial. But I also wanted to add page Numbers, somehting like that:
<<< 1 2 3 4 5 6 >>>
The code below works if i start my paging from 0, but not from 1. How can I fix this ?
Here is my code:
PaginatedList.cs
public class PaginatedList<T> : List<T> {
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = source.Count();
TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);
this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}
public bool HasPreviousPage {
get {
return (PageIndex > 0);
}
}
public bool HasNextPage {
get {
return (PageIndex+1 < TotalPages);
}
}
}
UserController.cs
public ActionResult List(int? page)
{
const int pageSize = 20;
IUserRepository userRepository = new UserRepository();
IQueryable<User> listUsers = userRepository.GetAll();
PaginatedList<User> paginatedUsers = new PaginatedList<User>(listUsers, page ?? 0, pageSize);
return View(paginatedUsers);
}
List.cshtml
#if (Model.HasPreviousPage)
{
#Html.RouteLink(" Previous ", "PaginatedUsers", new { page = (Model.PageIndex - 1) })
}
#for (int i = 1; i <= Model.TotalPages; i++)
{
#Html.RouteLink(#i.ToString(), "PaginatedUsers", new { page = (#i ) })
}
#if (Model.HasNextPage)
{
#Html.RouteLink(" Next ", "PaginatedUsers", new { page = (Model.PageIndex + 1) })
}
PaginatedList.cs
.Skip((PageIndex -1) * PageSize).Take(PageSize)
UserController.cs
public ActionResult List(int page = 1)
{
I have made this changes :
PaginatedList.cs
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
.
.
.
this.AddRange(source.Skip((PageIndex -1) * PageSize).Take(PageSize));
}
public bool HasPreviousPage {
get {
return (PageIndex > 1);
}
}
public bool HasNextPage {
get {
return (PageIndex < TotalPages);
}
}
UserController.cs
public ActionResult List(int? page)
{
const int pageSize = 20;
page = (page < 1) ? 1 : page ?? 0;
.
.
.
I would suggest to use a library instead of going to write your own pagination code. MvcPaging is one of those library.
See my answer here - what good libraries to use for paging on custom html (not table based)?
Edit: Entity Framework seems to be the issue, discussed further in question Entity Framework & Linq performance problem.
I am supporting a PagedList (using linq/generics) class written by someone long departed - and it has 2 lines that have very bad performance - on a dataset of just 2 thousand rows it takes up to one minute to run.
The two offending lines are:
TotalItemCount = source.Count();
I probably can fix that by passing in the count as a parameter. But the other line that is slow is the AddRange in this snippet:
IQueryable<T> a = source.Skip<T>((index) * pageSize).Take<T>(pageSize);
AddRange(a.AsEnumerable());
I don't understand why AddRange is so slow or what I can do to improve it?
The entire class source listing is
public class PagedList<T> : List<T>, IPagedList<T>
{
public PagedList(IEnumerable<T> source, int index, int pageSize)
: this(source, index, pageSize, null)
{
}
public PagedList(IEnumerable<T> source, int index, int pageSize, int? totalCount)
{
Initialize(source.AsQueryable(), index, pageSize, totalCount);
}
public PagedList(IQueryable<T> source, int index, int pageSize)
: this(source, index, pageSize, null)
{
}
public PagedList(IQueryable<T> source, int index, int pageSize, int? totalCount)
{
Initialize(source, index, pageSize, totalCount);
}
#region IPagedList Members
public int PageCount { get; private set; }
public int TotalItemCount { get; private set; }
public int PageIndex { get; private set; }
public int PageNumber { get { return PageIndex + 1; } }
public int PageSize { get; private set; }
public bool HasPreviousPage { get; private set; }
public bool HasNextPage { get; private set; }
public bool IsFirstPage { get; private set; }
public bool IsLastPage { get; private set; }
#endregion
protected void Initialize(IQueryable<T> source, int index,
int pageSize, int? totalCount)
{
//### argument checking
if (index < 0)
{
throw new ArgumentOutOfRangeException("PageIndex cannot be below 0.");
}
if (pageSize < 1)
{
throw new ArgumentOutOfRangeException("PageSize cannot be less than 1.");
}
//### set source to blank list if source is null to prevent exceptions
if (source == null)
{
source = new List<T>().AsQueryable();
}
//### set properties
if (!totalCount.HasValue)
{
TotalItemCount = source.Count();
}
PageSize = pageSize;
PageIndex = index;
if (TotalItemCount > 0)
{
PageCount = (int)Math.Ceiling(TotalItemCount / (double)PageSize);
}
else
{
PageCount = 0;
}
HasPreviousPage = (PageIndex > 0);
HasNextPage = (PageIndex < (PageCount - 1));
IsFirstPage = (PageIndex <= 0);
IsLastPage = (PageIndex >= (PageCount - 1));
//### add items to internal list
if (TotalItemCount > 0)
{
IQueryable<T> a = source.Skip<T>((index) * pageSize).Take<T>(pageSize);
AddRange(a.AsEnumerable());
}
}
}
It's probably not AddRange that is slow, it's probably the query over the source. The call to AsEnumerable() will come back immediately but the query actually won't be executed until inside of the AddRange call when the sequence is actually enumerated.
You can prove this by changing this line:
AddRange(a.AsEnumerable());
to:
T[] aa = a.ToArray();
AddRange(aa);
You will probably see that the ToArray() call is what takes most of the time because this is when the query is actually executed. Now why that is slow is anyone's guess. If you're using LINQ to SQL or Entity Framework, you can try profiling the database to see where the bottleneck is. But it's probably not the PagedList class.
Why don't you just make a an IEnumerable? Then you won't have to call AsEnumerable() at all.
Although come to think of it, the query doesn't actually execute until AsEnumerable is called, so it probably won't make any difference here.