Implement Pagination in ASP.NET Core 2.1 Web API - asp.net-core

I searched, but did't really found articles on how to implement pagination logic in an ASP.NET WebAPI Core 2.1 application...
I have the following
[Route("api/[controller]")]
[ApiController]
[EnableCors("AllowMyOrigin")]
public class EntriesController : ControllerBase
{
private readonly EntriesContext _context;
public EntriesController(EntriesContext context) {
_context = context;
if (_context.Entries.Count() == 0) {
_context.Entries.Add(new Entry { From = "default", To = "default" });
_context.SaveChanges();
}
}
[HttpGet]
public ActionResult<List<Entry>> GetAll() {
return _context.Entries.ToList();
}
[HttpGet("{id}", Name = "GetEntry")]
public ActionResult<Entry> GetById(long id) {
var item = _context.Entries.Find(id);
if (item == null) { return NotFound(); }
return item;
}
Now, I want my entries to be paginated using new params page and pageSize. Say
/api/entries?pageSize=3&page=2
Should I use the GetAll() method by adding some http params to it, or rather create a new method? There are no much sense to use page without pageSize, how do I manage this?

There are libraries, such as X.PagedList you can use. Frankly, pagination is pretty straight-forward, so you may not even need that. All you need to know is the page number, page size, and total number of results. The page number obvious comes from the request, and the page size can as well, if you want it customizable, or you can hard-code it.
public ActionResult<List<Entry>> GetAll(int page = 1, int size = 10)
Then, you can use Skip and Take to get the data for a particular page:
var query = _context.Entries;
var entries = await query.Skip((page - 1) * size).Take(size).ToListAsync();
var count = await query.CountAsync();
Then, all you need to know is the total number of pages, which can be calculated simply via:
var totalPages = (int)Math.Ceil(count / (float)size);
From that, you can calculate anything else you need, i.e.:
var firstPage = 1; // obviously
var lastPage = totalPages;
var prevPage = page > firstPage ? page - 1 : firstPage;
var nextPage = page < lastPage ? page + 1 : lastPage;

First of all, you can default you pageSize value to something:
[HttpGet]
public ActionResult<List<Entry>> GetAll(int? page = null, int? pageSize = 10)
{
if (!page.HasValue) {
return _context.Entries.ToList();
}
// do you pagination here
}
But you also may look at OData, it seems to be your case. It will allow you to query your data using http params, e.g.: /api/Entires?$skip=5&$top=5

I've just created a PagingTagHelper for ASP.NET Core Razor pages to render paging control easily with just the basic parameters, the simplest setup looks like below:
<paging total-records="Model.TotalRecords" page-no="Model.PageNo">
</paging>
all you need is to provide total records and page number for it to run. The default query string parameters are "p" for page number and "s" for page size, however, it is customizable/localizable, you can change all the settings according to your own requirements.
you can install it from nuget:
Install-Package LazZiya.TagHelpers -Version 1.0.2
then you need to add the tag helper to the _ViewImports.cshtml file:
#addTagHelper *, LazZiya.TagHelpers
http://ziyad.info/en/articles/21-Paging_TagHelper_for_ASP_NET_Core
more documentations and live demo will be available soon.

Related

How to deal with swagger against route overrides?

I'm trying to embed Swagger in my Asp Core (.Net 6) project where there are some cases of route overriding. However, the issue I'm facing can be reproduced even on the following case.
Consider a minimal Asp Core (.Net 6) app. For instance, just like the one used by Swashbuckle as test: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/test/WebSites/MinimalApp
Now, consider two controllers in the same app:
[ApiController]
[Route("/api")]
public class MyFallbackController : ControllerBase
{
[HttpGet("values", Order = 1)]
public ActionResult<object> GetValues()
{
return new[] { 1, 2, 3 };
}
}
[ApiController]
[Route("/api")]
public class MyOverrideController : ControllerBase
{
[HttpGet("values", Order = 0)]
public ActionResult<object> GetValues()
{
return new[] { 4, 5, 6 };
}
}
Notice that the routes are exactly the same, but only the first (Order = 0) will be considered.
If I run the app and navigate to:
https://localhost:7263/api/values
the response gives the expected result: [4, 5, 6]
However, when I try to access the Swagger section, it does not work because (apparently) it figures as a collision the controller pair:
An unhandled exception occurred while processing the request.
SwaggerGeneratorException: Conflicting method/path combination "GET
api/values" for actions -
WebApplication2.MyFallbackController.GetValues
(WebApplication2),WebApplication2.MyOverrideController.GetValues
(WebApplication2). Actions require a unique method/path combination
for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a
workaround
Is there any way to get rid of that problem?
Found it.
The trick is in the SwaggerGen configuration, as the exception message suggests, by the way.
using Microsoft.AspNetCore.Mvc.ApiExplorer;
services.AddSwaggerGen(c =>
{
c.ResolveConflictingActions(apiDescriptions =>
{
int best_order = int.MaxValue;
ApiDescription? best_descr = null;
foreach (var curr_descr in apiDescriptions)
{
int curr_order = curr_descr.ActionDescriptor.AttributeRouteInfo?.Order ?? 0;
if (curr_order < best_order)
{
best_descr = curr_descr;
}
}
return best_descr;
});
});
Basically, the above function selects only the ApiDescription with the lowest order among the duplicates.
That is my naive yet effective solution. For instance, I don't know if the input collection is given already sorted by order. In that case, the code could be even simpler.

Asp.net core Custom routing

I am trying to implement custom routing on an asp.net core application.
The desired result is the following:
http://Site_URL/MyController/Action/{Entity_SEO_Name}/
Entity_SEO_Name parameter will be a unique value saved into the database that it is going to help me identify the id of the entity that I am trying to display.
In order to achieve that I have implemented a custom route:
routes.MapMyCustomRoute(
name: "DoctorDetails",
template: " {controller=MyController}/{action=TestRoute}/{name?}");
public class MyTemplateRoute : TemplateRoute
{
public override async Task RouteAsync(RouteContext context)
{
//context.RouteData.Values are always empty. Here is the problem.
var seo_name = context.RouteData.Values["Entity_SEO_Name"];
int entityId = 0;
if (seo_name != null)
{
entityId = GetEntityIdFromDB(seo_name);
}
//Here i need to have the id and pass it to controller
context.RouteData.Values["id"] = entityId;
await base.RouteAsync(context);
}
}
My controller actionresult:
public ActionResult TestRoute(int id)
{
var entity = GetEntityById(id);
return Content("");
}
The problem with this approach is that the context.RouteData.Values are always empty.
Any ideas on how to move forward with this one ?
Your solution too complicated. You can have route template like
template: "{controller=Home}/{action=Index}/{seo?}"
and controller action just like
public ActionResult TestRoute(string seo)
{
var entity = GetEntityBySeo(seo);
return Content("");
}
It is enough, asp.net mvc is smart enough to bind seo variable to the parameter from url path.

Breeze OData: Get single entry with GetEntityByKey (EntitySetController)

Reading the documentation in Breeze website, to retrieve a single entity have to use the fetchEntityByKey.
manager.fetchEntityByKey(typeName, id, true)
.then(successFn)
.fail(failFn)
Problem 1: Metadata
When trying to use this method, an error is displayed because the metadata has not yet been loaded. More details about the error here.
The result is that whenever I need to retrieve a single entity, have to check if the metadata is loaded.
manager = new breeze.EntityManager(serviceName);
successFn = function(xhr) {}
failFn = function(xhr) {};
executeQueryFn = function() {
return manager.fetchEntityByKey(typeName, id, true).then(successFn).fail(failFn);
};
if (manager.metadataStore.isEmpty()) {
return manager.fetchMetadata().then(executeQueryFn).fail(failFn);
} else {
return executeQueryFn();
}
Question
How can I extend the breeze, creating a Get method to check if metadata is loaded, and if not, load it?
Problem 2: OData and EntitySetController
I would use the OData standard (with EntitySetController) in my APIs.
This page in Breeze documentation shows how, then follow this tutorial to deploy my app with OData.
The problem as you can see here and here, is that the EntitySetController follows the odata pattern, to retrieve an entity must use odata/entity(id), or to retrieve all entities you can use `odata/entity'.
Example
In controller:
[BreezeController]
public class passosController : EntitySetController<Passo>
{
[HttpGet]
public string Metadata()
{
return ContextProvider.Metadata();
}
[HttpGet, Queryable(AllowedQueryOptions = AllowedQueryOptions.All, PageSize = 20)]
public override IQueryable<T> Get()
{
return Repositorio.All();
}
[HttpGet]
protected override T GetEntityByKey(int key)
{
return Repositorio.Get(key);
}
}
When I use:
manager = new breeze.EntityManager("/odata/passos");
manager.fetchEntityByKey("Passo", 1, true)
.then(successFn)
.fail(failFn)
The url generated is: /odata/passos/Passos?$filter=Id eq 1
The correct should be: /odata/passos(2)
Question
How can I modify Breeze for when use fetchEntityByKey to retrieve entity use odata/entity(id)?

Naming conventions for view pages and setting controller action for view

I am unsure on how I should be naming my View pages, they are all CamelCase.cshtml, that when viewed in the browser look like "http://www.website.com/Home/CamelCase".
When I am building outside of .NET my pages are named like "this-is-not-camel-case.html". How would I go about doing this in my MVC4 project?
If I did go with this then how would I tell the view to look at the relevant controller?
Views/Home/camel-case.cshtml
Fake edit: Sorry if this has been asked before, I can't find anything via search or Google. Thanks.
There are a few ways you can do this:
Name all of your views in the style you would like them to show up in the url
This is pretty simple, you just add the ActionName attribute to all of your actions and specify them in the style you would like your url to look like, then rename your CamelCase.cshtml files to camel-case.cshtml files.
Use attribute routing
Along the same lines as above, there is a plugin on nuget to enable attribute routing which lets you specify the full url for each action as an attribute on the action. It has convention attributes to help you out with controller names and such as well. I generally prefer this approach because I like to be very explicit with the routes in my application.
A more framework-y approach
It's probably possible to do something convention based by extending the MVC framework, but it would be a decent amount of work. In order to select the correct action on a controller, you'd need to map the action name on its way in to MVC to its CamelCase equivalent before the framework uses it to locate the action on the controller. The easiest place to do this is in the Route, which is the last thing to happen before the MVC framework takes over the request. You'll also need to convert the other way on the way out so the urls generated look like you want them to.
Since you don't really want to alter the existing method to register routes, it's probably best write a function in application init that loops over all routes after they have been registered and wraps them with your new functionality.
Here is an example route and modifications to application start that achieve what you are trying to do. I'd still go with the route attribute approach however.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
WrapRoutesWithNamingConvention(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
private void WrapRoutesWithNamingConvention(RouteCollection routes)
{
var wrappedRoutes = routes.Select(m => new ConventionRoute(m)).ToList();
routes.Clear();
wrappedRoutes.ForEach(routes.Add);
}
private class ConventionRoute : Route
{
private readonly RouteBase baseRoute;
public ConventionRoute(RouteBase baseRoute)
: base(null, null)
{
this.baseRoute = baseRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var baseRouteData = baseRoute.GetRouteData(httpContext);
if (baseRouteData == null) return null;
var actionName = baseRouteData.Values["action"] as string;
var convertedActionName = ConvertHyphensToPascalCase(actionName);
baseRouteData.Values["action"] = convertedActionName;
return baseRouteData;
}
private string ConvertHyphensToPascalCase(string hyphens)
{
var capitalParts = hyphens.Split('-').Select(m => m.Substring(0, 1).ToUpper() + m.Substring(1));
var pascalCase = String.Join("", capitalParts);
return pascalCase;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var valuesClone = new RouteValueDictionary(values);
var pascalAction = valuesClone["action"] as string;
var hyphens = ConvertPascalCaseToHyphens(pascalAction);
valuesClone["action"] = hyphens;
var baseRouteVirtualPath = baseRoute.GetVirtualPath(requestContext, valuesClone);
return baseRouteVirtualPath;
}
private string ConvertPascalCaseToHyphens(string pascal)
{
var pascalParts = new List<string>();
var currentPart = new StringBuilder();
foreach (var character in pascal)
{
if (char.IsUpper(character) && currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
currentPart.Clear();
}
currentPart.Append(character);
}
if (currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
}
var lowers = pascalParts.Select(m => m.ToLower());
var hyphens = String.Join("-", lowers);
return hyphens;
}
}
}

MVC 4 How to get json response from a repository into a view using Ajax?

I am a newbie when it comes to MVC4 Web Development and there's something I am struggling with.
Basically, I have the following :
public class maincontroller: Controller
{
private MyRepository myRepository;
public mainController()
{
myRepository= new MyRepository();
}
public ActionResult Index()
{
var mystuff = myRepository.GetPrograms();
return View(mystuff);
}
public ActionResult MyStuff()
{
var mystuff = myRepository.GetStuff(1);
return Json(mystuff , JsonRequestBehavior.AllowGet);
}
}
Assuming that in my `MyRepository' class I have two functions:
One that is setting up `mystuff':
public MyRepository()
{
for (int i = 0; i < 8; i++)
{
programs.Add(new MyStuff
{
Title = "Hello" + i,
content = "Hi"
});
}
}
and second function that gets Stuff:
public List<MyStuff> GetStuff(int pageNumber = 0)
{
return stuff
.Skip(pageNumber * pageCount)
.Take(pageCount).ToList();
}
All works well. I mean I am able to iterate through `stuff' and display on a view...
The problem is that I want to display MyStuff() ( which returns Json ) using AJAX and then append all stuff to a view. How do I do that?
I have been beating my head against the wall for about 4 hours now, and can't get this working.
Please any help will be much appreciated.
Thank you.
At the most straightforward level, you can simply append HTML to your document using something like this (assuming you're using JQuery, because it's so much easier):
<div id="container"></div>
// make AJAX call to "MyStuff" action in the current controller
$.get(#Url.Action("MyStuff", function(data) {
// cycle through each item in the response
$.each(data, function(index, item) {
// construct some HTML from the JSON representation of MyStuff
var html = "<div>" + item.StuffProperty + "</div>";
// append the HTML to a container in the current document
$("#container").append(html);
});
});
This adds some HTML for each item in the collection to a container element, using (eg) StuffProperty from the MyStuff class.
Appending HTML manually like this can be a hassle once it gets too complicated -- at that point you should consider using either:
Partial views (return HTML directly from the controller, instead of JSON)
A client-side templating engine like Mustache.js, Underscore.js, etc, to convert JSON into HTML.