Pagination In WebAPI - c#

Currently I have following way of providing pagination for every entity/resource for a set of APIs:
public IHttpActionResult GetPageUsers(int startindex, int size, string sortby = "Username", string order = "DESC")
{
if (!order.Equals("ASC") && !order.Equals("DESC"))
return BadRequest("Order Has To Be DESC or ASC");
if (!new string[] { "username", "name" }.Contains(sortby.ToLower()))
return BadRequest("Not A Valid Sorting Column");
return Ok(new
{
records = Mapper.Map<List<User>, List<HomeBook.API.Models.User.UserDTO>>(
db.Users.OrderBy(sortby + " " + order).Skip(startindex).Take(size).ToList()
),
total = db.Users.Count()
});
}
But I would like to move the validation logic to Model so that I can reply back with ModelState errors from one of my Action filters and the request would not even need to get inside the controller.
But I cannot also change the action signature to expect an object parameter to what I have already otherwise, I get multiple action error.
Basically, what I am asking is how I can remove validation logic from here to somewhere else so that the request does not even need to get inside the action. I would really appreciate some insight on this problem or even pagination in WebAPIs in general

You could define an action filter attribute to perform the validation:
public class ValidatePagingAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
string order = (string) actionContext.ActionArguments["order"];
string sortBy = (string) actionContext.ActionArguments["sortby"];
var states = new ModelStateDictionary();
if(!order.Equals("ASC") && !order.Equals("DESC"))
{
states.AddModelError("order", "Order has to be DESC or ASC");
}
if (!new[] { "username", "name" }.Contains(sortBy.ToLower()))
{
states.AddModelError("sortby", "Not A Valid Sorting Column");
}
if(states.Any())
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, states);
}
}
}
And then use it like so:
[ValidatePaging]
public IHttpActionResult GetPageUsers(int startindex, int size, string sortby = "Username", string order = "DESC")
{
// stuff
}
Also, have a look at http://bitoftech.net/2013/11/25/implement-resources-pagination-asp-net-web-api on how to implement paging in web api.

Related

Kendo Grid changing depending on DropDownList

Before I start I'll just say that I've looked at other answers before posting and none specifically help me.
I need to create a Kendo UI grid in ASP.NET MVC that changes depending on what the users selects from a DropDownList. I will eventually be using data from a database, but currently I'm trying to learn with random hard-coded data.
I found a tutorial online that shows me how to do it with data from a sample database, but I can't set that up for reasons I cant explain. So I'm trying to adapt the code from that tutorial to work with my controllers and models. This might be set up completely wrong as I'm relatively new to ASP.NET MVC.
So here's the tutorial I'm trying to follow.
This is my controller:
public class LookupValueController : Controller
{
private List<LookupModel> tables = new
List<LookupModel>()
{ new LookupModel() { TableName = "Table1",
Description = "Table 1" },
new LookupModel() { TableName = "Table2",
Description = "Table 2" } };
private List<LookupValueModel> values = new List<LookupValueModel>()
{ new LookupValueModel() { TableName = "Table1", Description = "Value 1", LookupCode = "1" },
new LookupValueModel() { TableName = "Table2", Description = "Value 2", LookupCode = "2"} };
// GET: LookupValue
public ActionResult Index()
{
return View();
}
public ActionResult GetAllTableA()
{
try
{
var table = tables;
return Json(table, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(ex.Message);
}
}
public ActionResult GetAllTableB()
{
try
{
var value = values;
return Json(value, JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(ex.Message);
}
}
}
Then my 2 models:
public class LookupValueModel
{
public string TableName { get; set; }
public string LookupCode { get; set; }
public string Description { get; set; }
}
public class LookupModel
{
public string TableName { get; set; }
public string Description { get; set; }
}
I've tried just changing the values in the view in the tutorial but it doesn't work, as I believe it isn't as simple as just changing some text.
I'm pretty stuck for how to do this and don't know where to go from here. I know this is a very long winded post with lots of code, but I would really appreciate some help.
Where am I going wrong adapting the tutorial code? What do I have to change to get it to work with hard-coded data?
That's not that hard. What you need to do is to change the DataSource's url for each Action you want. So, depending on what options user selects in the DDL, you change the DataSource. Check this demo.
What you need to change in from the above demo is that your grid's DataSource will call an url instead of a hard-coded json, right? In that url, you change the desired action:
let changeTableData = function changeTableData(option) {
let dataSource = new kendo.data.DataSource({
transport: {
url: "MyApp/" + option
}
});
$("#grid").data("kendoGrid").setDataSource(dataSource);
};
It will read the new url and fetch the data into the grid and updated it.
UPDATE
The transport url ir the url path to your action, e.g.
let url;
if (option == "A") {
url = "#Url.Action("TableA")";
}
else if (option == "B") {
url = "#Url.Action("TableB")";
}
let dataSource = new kendo.data.DataSource({
transport: {
url: url
}
});
1) Remove the grid from this view and create a new partialview and just have the grid located in that.
Now this can be one of two ways. Either an onclick via the drop down list or an onchange. Your choice
function Getdropdown()
{
var id = $("#//dropdownID").val(); //Get the dropdown value
var json = '{dropdownId: ' + id + '}';
$.ajax({
url:'#Url.Action("ViewGrid", "//Controller")',
type:'POST',
data:json,
contentType:'Application/json',
success:function(result){
$("//The Id of of the div you want the partial to be displayed in in the cshtml").html(result);
}
});
}
2) Get the value of the dropdown and pass it to a controller method that calls this new partial view, sending it the ID in a model
public ActionResult ViewGrid(int dropdownId)
{
AModel model = new AModel
{
DropDownID = dropdownId
};
return PartialView("_theGridPartial", model);
}
3) Change your grid to look like this:
#(Html.Kendo().Grid<KendoMvcApp.Models.EmployeeA>()
.Name("EmpGrid")
.Selectable()
.Columns(columns =>
{
columns.Bound(c => c.FirstName);
columns.Bound(c => c.LastName);
})
.DataSource(dataSource => dataSource
.Ajax()
.Read(read => read.Action("GetAllEmployee", "GridDataSource", new {id = Model.DropDownID}))
)
)
4) This is the new Controller read
public ActionResult GetAllEmployee([DataSourceRequest]DataSourceRequest request, int id)
{
try
{
//Have a call that gets the table data based on the id you are passing into here. This id will be the table id you have got from your dropdown list
}
catch (Exception ex)
{
return Json(ex.Message);
}
}
This should allow you to change the table based on the dropdown.

Fluent Validation changing CustomAsync to MustAsync

Could some one please help me to resolved this? i'm trying to change CustomAsync to MustAsync, but i couldn't make things to work. Below is my custom method
RuleFor(o => o).MustAsync(o => {
return CheckIdNumberAlreadyExist(o)
});
private static async Task<ValidationFailure> CheckIdNumberAlreadyExist(SaveProxyCommand command)
{
if (command.Id > 0)
return null;
using (IDbConnection connection = new SqlConnection(ConnectionSettings.LicensingConnectionString))
{
var param = new DynamicParameters();
param.Add("#idnumber", command.IdNumber);
var vehicle = await connection.QueryFirstOrDefaultAsync<dynamic>("new_checkDuplicateProxyIdNumber", param, commandType: CommandType.StoredProcedure);
return vehicle != null
? new ValidationFailure("IdNumber", "Id Number Already Exist")
: null;
}
}
To make it work with the latest version of the FluentValidation, I had to use the codes like below.
RuleFor(ws => ws).MustAsync((x, cancellation) => UserHasAccess(x)).WithMessage("User doesn't have access to perform this action");
Please notice the lambda expression here MustAsync((x, cancellation) => UserHasAccess(x)), without this I was always getting an error as cannot convert from 'method group' to 'Func<Worksheet, CancellationToken, Task<bool>>
Below is my custom UserHasAccess function.
private async Task <bool> UserHasAccess(Worksheet worksheet) {
var permissionObject = await _dataProviderService.GetItemAsync(worksheet.FileItemId);
if (permissionObject is null) return false;
if (EditAccess(permissionObject.Permission)) return true;
return false;
}
I'm assuming you're using a version of FluentValidation prior to version 6, as you're not passing in a Continuation Token, so I've based my answer on version 5.6.2.
Your example code does not compile, for starters, as you're missing a semi-colon in your actual rule. You are also evaluating two different properties on the SaveProxyCommand parameter.
I've built a very small POC based on some assumptions:
Given 2 classes:
public class SaveProxyCommand {
public int Id { get; set; }
}
public class ValidationFailure {
public string PropertyName { get; }
public string Message { get; }
public ValidationFailure(string propertyName, string message){
Message = message;
PropertyName = propertyName;
}
}
And a validator:
public class SaveProxyCommandValidator : AbstractValidator<SaveProxyCommand>{
public SaveProxyCommandValidator()
{
RuleFor(o => o).MustAsync(CheckIdNumberAlreadyExists)
.WithName("Id")
.WithState(o => new ValidationFailure(nameof(o.IdNumber), "Id Number Already Exist"));
}
private static async Task<bool> CheckIdNumberAlreadyExists(SaveProxyCommand command) {
if (command.Id > 0)
return true;
var existingIdNumbers = new[] {
1, 2, 3, 4
};
// This is a fudge, but you'd make your db call here
var isNewNumber = !(await Task.FromResult(existingIdNumbers.Contains(command.IdNumber)));
return isNewNumber;
}
}
I didn't include the call to the database, as that's not part of your problem. There are a couple of things of note here:
You're not setting the .WithName annotation method, but when you're setting up a validation rule for an object you have to do this, as FluentValidation expects you to specify specific properties to be validated by default, if you pass in an entire object it just doesn't know how to report errors back.
Must/MustAsync need to return a bool/Task<bool> instead of a custom object. To get around this, you can specify a custom state to be returned when failing validation.
You can then get access to this like this:
var sut = new SaveProxyCommand { Id = 0, IdNumber = 3 };
var validator = new SaveProxyCommandValidator();
var result = validator.ValidateAsync(sut).GetAwaiter().GetResult();
var ValidationFailures = result.Errors?.Select(s => s.CustomState).Cast<ValidationFailure>();
The above does not take into account empty collections, it's just an example of how to dig into the object graph to retrieve custom state.
As a suggestion, fluentvalidation works best if you set up individual rules per property, instead of validating the entire object. My take on this would be something like this:
public class SaveProxyCommandValidator : AbstractValidator<SaveProxyCommand>{
public SaveProxyCommandValidator()
{
RuleFor(o => o.IdNumber).MustAsync(CheckIdNumberAlreadyExists)
.Unless(o => o.Id > 0)
.WithState(o => new ValidationFailure(nameof(o.IdNumber), "Id Number Already Exist"));
}
private static async Task<bool> CheckIdNumberAlreadyExists(int numberToEvaluate) {
var existingIdNumbers = new[] {
1, 2, 3, 4
};
// This is a fudge, but you'd make your db call here
var isNewNumber = !(await Task.FromResult(existingIdNumbers.Contains(numberToEvaluate)));
return isNewNumber;
}
}
This read more like a narrative, it uses the .Unless construct to only run the rule if Id is not more than 0, and does not require the evaluation of the entire object.

Accept a list (of unknown length) of query string parameters to MVC action

I am building a page which will have a query string sent to it containing any number of productId_{index} and productQuantity_{index}.
E.g:
http://www.website.com/action?productId_1=65&productQuantity_1=1&productId_2=34&productQuantity_2=1
So this URL would map a ProductId of 65 to a quantity of 1 and a ProductId of 34 to a quantity of 1.
I cannot change how this is sent to the page which I why I have not used a solution like this.
I would like to be able to somehow map the query in this format to a strongly typed list of objects.
This question is meant to be mainly asking about the MVC way to do this, as I already have a solution using the Request object, but being able to do this the MVC way would be preferable.
you need to format the query string to something simpler and easy to read like this: p=id-quantity, then you can use params,
e.g.: http://www.website.com/action?p=65-1&p34-4&p=32-23&p=....
public ActionResult Products(params string[] p)
{
foreach(var product in p)
{
var productId = product.Split('-')[0];
var quantity = product.Split('-')[1];
}
}
[Notice]
I don't recommend sending such parameters via url "GET", it is better and safer if you use "POST" form method.
As was suggested to me, I ended up using a custom model binder to give me a list of objects in my Action.
Custom Model Binder
public class ProductIdAndQuantityListModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var request = controllerContext.HttpContext.Request;
var products = new List<ProductIdAndQuantity>();
for (int i = 0; i < 25; i++)
{
string productIdKey = string.Format("productid_{0}", i);
string quantityKey = string.Format("productqty_{0}", i);
string productIdVal = request[productIdKey];
string quantityVal = request[quantityKey];
if (productIdVal == null || quantityVal == null)
break;
int productId = Convert.ToInt32(productIdVal);
int quantity = Convert.ToInt32(quantityVal);
var productIdAndQuantity = products.FirstOrDefault(x => productId == x.ProductId);
if (productIdAndQuantity != null)
{
productIdAndQuantity.Quantity += quantity;
}
else
{
products.Add(new ProductIdAndQuantity()
{
ProductId = productId,
Quantity = quantity
});
}
}
return products;
}
}
Global.asax.cs
protected void Application_Start()
{
ModelBinders.Binders.Add(typeof(ICollection<Models.Basket.ProductIdAndQuantity>), new ProductIdAndQuantityListModelBinder());
}
Action
public ActionResult Index(ICollection<ProductIdAndQuantity> products)
{
foreach (var product in products)
{
// Do stuff...
}
}
Thank you you all for your help! As you can see it's an unknown but not unlimited number of parameters it could take which is just down to how I'm using it I think.

Restful Web Api parameter as an array of int

Ideally I would like to have an URL in following format:
/api/categories/1,2,3...N/products
And this would return all products for the specified categories. Having one API call with multiple category IDs saves me several database calls, thus improves performance.
I can easily implement this in a following way.
public HttpResponseMessage GetProducts(string categoryIdsCsv)
{
// <1> Split and parse categoryIdsCsv
// <2> Get products
}
However, this doesn't look like a clean clean solution, and possibly breaking SRP principle. I also tried using ModelBinder, however it adds parameters to query string.
Questions:
Is there a clean way to implement such URL structure?
Or is there a different/better approach to retrieve all products for multiple categories?
Please let me know if you need any further clarification.
I've just found an answer to my question. Route attribute had missing parameter when using ModelBinder.
[Route("api/categories/{categoryIds}/products")]
public HttpResponseMessage GetProducts([ModelBinder(typeof(CategoryIdsModelBinder))] CategoryIds categoryIds)
{
// <2> Get products using categoryIds.Ids
}
And CategoryIds would be
public class CategoryIds
{
public List<int> Ids{ get; set; }
}
And CategoryIdsModelBinder would be
public class CategoryIdsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(CategoryIds))
{
return false;
}
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
var key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
return false;
}
var values = val.AttemptedValue.Split(',');
var ids = new List<int>();
foreach (var value in values)
{
int intValue;
int.TryParse(value.Trim(), out intValue);
if (intValue > 0)
{
ids.Add(intValue);
}
}
if (ids.Count > 0)
{
var result = new CategoryIds
{
Ids= ids
};
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
We can use Post methods
[RoutePrefix ( "api/categories" )]
public class TestController
{
[HttpPost]
[Route ( "getProducts" )]
public HttpResponseMessage GetProducts ( HttpRequestMessage request )
{
HttpResponseMessage message = null;
string input = string.Empty;
input = request.Content.ReadAsStringAsync ().Result;
var ids = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>> ( input );
}
}
Unfortunately Web API can not parse your data as array or as some kind of your custom object out of the box.
If you want to parse your url param as array you can try to do:
Write your own route constraint which will read and convert your param from string to array of ints/strings/whatever;
Write your custom type converter and use it with your data model;
write your value provider and also use it with your data model
Use parameter binding
Moreover you can always use query params which is never will break principles of REST :)
Please see more details about here and here
Hope that helps

When using jqgrid in asp.net-mvc site, is there a way to include the filter in the url so you can share filtered views?

I have an asp.net-mvc site and I user jqgrid on the front end. I have a simple page using jqgrid and I filter down my jqgrid results (server side filter) using the top bar filter of the advanced filter.
I now want a way where I can share a URL with someone else and when they load the page, they get the same filter applied so somehow I need to take the filter criteria and append it to the query string.
The issue is that I can do this "manually" field by field like this by creating queryparams like
myurl?NameFilter=JoeBrown
and then doing something like this in my asp.net-mvc view
var myfilter = { groupOp: "AND", rules: [] };
<% if (!String.IsNullOrEmpty(Model.NameFilter)) { %>
myfilter.rules.push({ field: "Name", op: "eq", data: "<% = Model.NameFilter%>" });
<%}%>
but that doesn't really scale very well given I have many different pages with lots of columns so I am looking for a more generic way to persist the filter values into a URL and then apply them again so that I can then model bind on the server side back to my controller action.
Here is an example Server Side controller action that I am calling to load data from the server:
public ActionResult GridData(GridData args)
{
var data = GetData(args).Paginate(args.page ?? 1, args.rows ?? 10,
i =>
new
{
i.Id,
i.Name
}
}
so basically I need the query string to bind to my GridData class similar to what happens when I do a normal filter that gets posted on the ajax call when I do a regular filter.
My GridData class looks like this:
public class GridData
{
public int? page { get; set; }
public int? rows { get; set; }
public bool search { get; set; }
public string sidx { get; set; }
public string sord { get; set; }
public Filter Where { get; set; }
}
public class Filter
{
public string groupOp { get; set; }
public Rule[] rules { get; set; }
}
public class Rule
{
public string field { get; set; }
public string op { get; set; }
public string data { get; set; }
}
If you are looking for a way to construct a model that will bind directly from query string values, you can try the following. The methods buildParamsCustom() and serializeJson() are basically taken from jQuery source and modified for creating a query string which will be supported by the MVC default model binder.
http://api.jquery.com/jquery.param/ has the details about the default jQuery implementation.
I have tested for your scenario. I am able to view the serialized data.
var r20 = /%20/g, rbracket = /\[\]$/;
function buildParamsCustom(prefix, obj, traditional, add) {
var name;
if (jQuery.isArray(obj)) {
// Serialize array item.
jQuery.each(obj, function (i, v) {
if (traditional || rbracket.test(prefix)) {
// Treat each array item as a scalar.
add(prefix, v);
} else {
// Item is non-scalar (array or object), encode its numeric index.
buildParamsCustom(prefix + "[" + (typeof v === "object" ? i : "") + "]", v, traditional, add);
}
});
} else if (!traditional && jQuery.type(obj) === "object") {
// Serialize object item.
for (name in obj) {
buildParamsCustom(prefix + "." + name, obj[name], traditional, add);
}
} else {
// Serialize scalar item.
add(prefix, obj);
}
}
// Serialize an array of form elements or a set of
// key/values into a query string
var serializeJson = function (a, traditional) {
var prefix,
s = [],
add = function (key, value) {
// If value is a function, invoke it and return its value
value = jQuery.isFunction(value) ? value() : (value == null ? "" : value);
s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
};
// Set traditional to true for jQuery <= 1.3.2 behavior.
if (traditional === undefined) {
traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
}
// If an array was passed in, assume that it is an array of form elements.
if (jQuery.isArray(a) || (a.jquery && !jQuery.isPlainObject(a))) {
// Serialize the form elements
jQuery.each(a, function () {
add(this.name, this.value);
});
} else {
// If traditional, encode the "old" way (the way 1.3.2 or older
// did it), otherwise encode params recursively.
for (prefix in a) {
buildParamsCustom(prefix, a[prefix], traditional, add);
}
}
// Return the resulting serialization
return s.join("&").replace(r20, "+");
};
// Get this data from the grid
var data = { "page": 1, "rows": 10, "search": true, "Where": { "groupop": "AND", "rules": [{ "field": "Name", "op": "EQ", data: "John" }, { "field": "Title", "op": "EQ", data: "Mr" }] } };
var queryString = serializeJson(data);
var url = "someurl" + "?" + decodeURIComponent(queryString);
// Send your GET request here.

Categories

Resources