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
Related
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.
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.
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.
I have this (simplified) class:
public class StarBuildParams
{
public int BaseNo { get; set; }
public int Width { get; set; }
}
And I have to transform instances of it to a querystring like this:
"BaseNo=5&Width=100"
Additionally I have to transform such a querystring back in an object of that class.
I know that this is pretty much what a modelbinder does, but I don't have the controller context in my situation (some deep buried class running in a thread).
So, is there a simple way to convert a object in a query string and back without having a controller context?
It would be great to use the modelbinding but I don't know how.
A solution with Newtonsoft Json serializer and linq:
string responseString = "BaseNo=5&Width=100";
var dict = HttpUtility.ParseQueryString(responseString);
string json = JsonConvert.SerializeObject(dict.Cast<string>().ToDictionary(k => k, v => dict[v]));
StarBuildParams respObj = JsonConvert.DeserializeObject<StarBuildParams>(json);
You can use reflection, something like this:
public T GetFromQueryString<T>() where T : new(){
var obj = new T();
var properties = typeof(T).GetProperties();
foreach(var property in properties){
var valueAsString = HttpContext.Current.Request.QueryString[property.PropertyName];
var value = Parse( valueAsString, property.PropertyType);
if(value == null)
continue;
property.SetValue(obj, value, null);
}
return obj;
}
You'll need to implement the Parse method, just using int.Parse, decimal.Parse, DateTime.Parse, etc.
Use this Parse method with the ivowiblo's solution (accepted answer):
public object Parse(string valueToConvert, Type dataType)
{
TypeConverter obj = TypeDescriptor.GetConverter(dataType);
object value = obj.ConvertFromString(null, CultureInfo.InvariantCulture, valueToConvert);
return value;
}
You can set the properties of this object in its constructor by retrieving the relevant values from the querystring
public StarBuildParams()
{
this.BaseNo = Int32.Parse(Request.QueryString["BaseNo"].ToString());
this.Width = Int32.Parse(Request.QueryString["Width"].ToString());
}
and you can ensure that the object is converted to the correct querystring format by overriding the ToString method.
public override string ToString()
{
return String.Format("BaseNo={0}&Width={1}", this.BaseNo, this.Width);
}
You'll still need to construct and call ToString in the appropriate places, but this should help.
You can just use .NET's HttpUtility.ParseQueryString() method:
HttpUtility.ParseQueryString("a=b&c=d") produces a NameValueCollection as such:
[0] Key = "a", Value = "b"
[1] Key = "c", Value = "d"
This should work so long as none of the properties match any other route parameters like controller, action, id, etc.
new RouteValueDictionary(Model)
http://msdn.microsoft.com/en-us/library/cc680272.aspx
Initializes a new instance of the RouteValueDictionary class and adds
values that are based on properties from the specified object.
To parse back from the query string you can use the model class as an action parameter and let the ModelBinder do it's job.
Serialize query string and deserialize to your class object
JObject json;
Request.RequestUri.TryReadQueryAsJson(out json);
string sjson = JsonConvert.SerializeObject(json);
StarBuildParams query = JsonConvert.DeserializeObject<StarBuildParams>(sjson);
Building off of Ivo and Anupam Singh's great solutions above, here is the code that I used to turn this into a base class for POST requests (in the event that you may only have the raw query string like in a Web API setup). This code works for lists of objects, but could easily be modified to parse a single object.
public class PostOBjectBase
{
/// <summary>
/// Returns a List of List<string> - one for each object that is going to be parsed.
/// </summary>
/// <param name="entryListString">Raw query string</param>
/// <param name="firstPropertyNameOfObjectToParseTo">The first property name of the object that is sent in the list (unless otherwise specified). Used as a key to start a new object string list. Ex: "id", etc.</param>
/// <returns></returns>
public List<List<string>> GetQueryObjectsAsStringLists(string entryListString, string firstPropertyNameOfObjectToParseTo = null)
{
// Decode the query string (if necessary)
string raw = System.Net.WebUtility.UrlDecode(entryListString);
// Split the raw query string into it's data types and values
string[] entriesRaw = raw.Split('&');
// Set the first property name if it is not provided
if (firstPropertyNameOfObjectToParseTo == null)
firstPropertyNameOfObjectToParseTo = entriesRaw[0].Split("=").First();
// Create a list from the raw query array (more easily manipulable) for me at least
List<string> rawList = new List<string>(entriesRaw);
// Initialize List of string lists to return - one list = one object
List<List<string>> entriesList = new List<List<string>>();
// Initialize List for current item to be added to in foreach loop
bool isFirstItem = false;
List<string> currentItem = new List<string>();
// Iterate through each item keying off of the firstPropertyName of the object we will ultimately parse to
foreach (string entry in rawList)
{
if (entry.Contains(firstPropertyNameOfObjectToParseTo + "="))
{
// The first item needs to be noted in the beginning and not added to the list since it is not complete
if (isFirstItem == false)
{
isFirstItem = true;
}
// Finished getting the first object - we're on the next ones in the list
else
{
entriesList.Add(currentItem);
currentItem = new List<string>();
}
}
currentItem.Add(entry);
}
// Add the last current item since we could not in the foreach loop
entriesList.Add(currentItem);
return entriesList;
}
public T GetFromQueryString<T>(List<string> queryObject) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties();
foreach (string entry in queryObject)
{
string[] entryData = entry.Split("=");
foreach (var property in properties)
{
if (entryData[0].Contains(property.Name))
{
var value = Parse(entryData[1], property.PropertyType);
if (value == null)
continue;
property.SetValue(obj, value, null);
}
}
}
return obj;
}
public object Parse(string valueToConvert, Type dataType)
{
if (valueToConvert == "undefined" || valueToConvert == "null")
valueToConvert = null;
TypeConverter obj = TypeDescriptor.GetConverter(dataType);
object value = obj.ConvertFromString(null, CultureInfo.InvariantCulture, valueToConvert);
return value;
}
}
Then you can inherit from this class in wrapper classes for POST requests and parse to whichever objects you need. In this case, the code parses a list of objects passed as a query string to a list of wrapper class objects.
For example:
public class SampleWrapperClass : PostOBjectBase
{
public string rawQueryString { get; set; }
public List<ObjectToParseTo> entryList
{
get
{
List<List<string>> entriesList = GetQueryObjectsAsStringLists(rawQueryString);
List<ObjectToParseTo> entriesFormatted = new List<ObjectToParseTo>();
foreach (List<string> currentObject in entriesList)
{
ObjectToParseToentryPost = GetFromQueryString<ObjectToParseTo>(currentObject);
entriesFormatted.Add(entryPost);
}
return entriesFormatted;
}
}
}
In my REST WCF service I am passing nearly 15 parameters. I am passing these parameters in the URL like this:
www.mysite.com/wcfservice/mymethod/{p1},{p2},{p3},{p4}...
Is there a better way of passing parameters? Does passing parameters using in the URL cause any security issues (like SQL injection)? Is it wise to pass the parameters using an XML file instead? What is the best way to pass the parementers in a REST WCF service?
Assuming your method is Idempotent (i.e. GET) it seems you know you can't use the body to transfer. So you're left with the URL and Headers.
Put in the Headers the information that is not contextual to this specific request - e.g. your ProtocolVersion, SystemName - and parse those headers in the Service.
In the URL put those parameters that are contextual and are required for you to execute your operation: e.g. EntityId, FilterValue.
If you are passing a list for one parameter - e.g. value1=1,2,3 - then you can consider using a custom QueryString Converter (see below - attaching the behavior to the Endpoint is another exercise).
And in the end, you may just have to pass that many parameters. It's very common for Search-based operations where there may be various dimensions to search on.
using System;
using System.Linq;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
public class CustomQueryStringConverter : QueryStringConverter
{
public override bool CanConvert(Type type)
{
return base.CanConvert(type.IsArray ? type.GetElementType() : type);
}
public override object ConvertStringToValue(string parameter, Type parameterType)
{
object result = null;
if (parameterType.IsArray)
{
if (!ReferenceEquals(parameter, null))
{
object[] items = parameter
.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => base.ConvertStringToValue(s.Trim(), parameterType.GetElementType()))
.ToArray();
Array arrayResult = Array.CreateInstance(parameterType.GetElementType(), items.Length);
for (int i = 0; i < items.Length; ++i)
{
arrayResult.SetValue(items[i], i);
}
result = arrayResult;
}
}
else
{
result = base.ConvertStringToValue(parameter, parameterType);
}
return result;
}
public override string ConvertValueToString(object parameter, Type parameterType)
{
string result = string.Empty;
if (parameterType.IsArray)
{
foreach (object item in (Array)parameter)
{
result += item.ToString() + ",";
}
result = result.TrimEnd(',');
}
else
{
result = base.ConvertValueToString(parameter, parameterType);
}
return result;
}
public class CustomQueryStringBehavior : WebHttpBehavior
{
protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
{
return new CustomQueryStringConverter();
}
}
}