c# JSONPatch not working with int and enums - c#

I am try to implement ASP.NET MVC 5 WebAPI Patch. But there is a problem with int values and enums. JSON deserialize "think" that is better convert int to long, so is because that in my int propertie i recive allways 0...
So i found "Microsoft.AspNet.JsonPatch.JsonPatchDocument" (there has many others out ther)
Then problem is, i recive allways null in my model
public async Task<IHttpActionResult> Patch( int id, [FromBody] Microsoft.AspNet.JsonPatch.JsonPatchDocument<Customer> model)
{
//model is allways == null
}
I am using POSTMAN to send the patch as json in Body>Raw. Header is application/json.
I donĀ“t understand why model is null... i need to do anything on WebApiConfig?
Still I tried to implements a custom JsonConverter, but the problem is i have a lots of Enum types, i need to create one for each enum? I try sothing like this:
public class Int32EnumConverter<T> : JsonConverter
but the problem is in WebApiConfig.cs you need to implement this:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Int32EnumConverter<[I NEED HEAR Dynamic Enum Types>>);
There as any one to help me? Thanks!

Found! I did my on Delta Patch
public class JSONPatch<T>
{
private Dictionary<string, object> propsJson = new Dictionary<string, object>();
public JSONPatch()
{
Stream req = HttpContext.Current.Request.InputStream;
req.Seek(0, System.IO.SeekOrigin.Begin);
string json = new StreamReader(req).ReadToEnd();
propsJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);
}
public JSONPatch(object model)
{
propsJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(model.ToString());
}
public void Patch(T model)
{
PropertyInfo[] properties = model.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
try
{
if (!propsJson.Any(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase)))
continue;
KeyValuePair<string, object> item = propsJson.First(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase));
Type targetProp = model.GetType().GetProperty(property.Name).PropertyType;
Type targetNotNuable = Nullable.GetUnderlyingType(targetProp);
if (targetNotNuable != null)
{
targetProp = targetNotNuable;
}
if (item.Value.GetType() != typeof(Int64))
{
object newA = Convert.ChangeType(item.Value, targetProp);
model.GetType().GetProperty(property.Name).SetValue(model, newA, null);
}
else
{
int value = Convert.ToInt32(item.Value);
if (targetProp.IsEnum)
{
object newA = Enum.Parse(targetProp, value.ToString());
model.GetType().GetProperty(property.Name).SetValue(model, newA, null);
}
else
{
object newA = Convert.ChangeType(value, targetProp);
model.GetType().GetProperty(property.Name).SetValue(model, newA, null);
}
}
}
catch
{
}
}
}
}
and now:
public async Task<IHttpActionResult> Patch( int id, [FromBody]JSONPatch<Customer> model)
{
....
Atention: not 100% tested! fail for null values.

Related

"Unwrapping" posted JSON

I'm looking for a way to "unwrap" JSON that's been posted to a MVC Core service. Let's say, I have the following method:
[HttpPost]
public dynamic SayHello(string FirstName, string SecondName)
{
return $"Hello {FirstName} {SecondName} !";
}
And I post in the following JSON:
{
"FirstName":"Joe",
"SecondName": "Bloggs"
}
I'd expect to get a response of Hello Joe Bloggs !, but I cannot seem to find an easy way to unwrap the JSON object into the properties of the method.
I know the correct solution is to have a HelloModel with those two properties in, slap on a [FromBody] attribute, but for reasons this isn't possible.
Here's another (slightly convoluted) option. You can create your own action filter to intercept the request and populate the parameters with values based on deciding the JSON yourself. For example:
public class JsonToParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var stream = filterContext.HttpContext.Request.Body;
using (var sr = new StreamReader(stream))
using (var jsonTextReader = new JsonTextReader(sr))
{
var serializer = new JsonSerializer();
var body = serializer.Deserialize<JObject>(jsonTextReader);
if (body == null) return;
foreach (var parameter in filterContext.ActionDescriptor.Parameters)
{
var jsonProperty = body.Properties().SingleOrDefault(p => p.Name == parameter.Name);
if (jsonProperty != null)
{
var param = filterContext.ActionDescriptor.Parameters.OfType<ControllerParameterDescriptor>().FirstOrDefault(e => e.Name == parameter.Name);
if (param == null)
{
continue;
}
if (!filterContext.ActionArguments.ContainsKey(parameter.Name))
{
object value;
try
{
value = jsonProperty.Value.ToObject(param.ParameterInfo.ParameterType);
}
catch (Exception)
{
value = GetDefault(param.ParameterInfo.ParameterType);
}
filterContext.ActionArguments.Add(parameter.Name, value);
}
}
}
}
}
private static object GetDefault(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
}
Now decorate your action method with this new attribute:
[HttpPost]
[JsonToParameters]
public dynamic SayHello(string FirstName, string SecondName)
{
return $"Hello {FirstName} {SecondName} !";
}

Custom property binding in [FromUri] object

I use DTO objects as the parameters of requests to my JSON REST webservice, which is being migrated from WCF.
To get the products with the id=1 or the id=3 I have been using this Uri:
http://example.com/products?ids=1,3
For simplicity, I want to use existent DTO classes as [FromUri] parameter of my controller methods. For instance:
[HttpGet]
[Route("products")]
public IHttpActionResult GetProducts([FromUri] GetProductRequestParameters parameters)
{
...
}
And this is the DTO:
public class GetProductRequestParameters
{
public IEnumerable<int> Ids { get; set; }
public int FamilyId { get; set; }
}
The problem is that model binder expects something like ?Ids=1&Ids=3 instead of a "comma separated value" like ?Ids=1,3 for the property IEnumerable<int>
With the following code I've achieved to bind this kind of data if I use querystring parameters in the controller method instead of the DTO. I would prefer to use the later because DTOs can have lots of properties and I don't want to be filling in every parameter manually.
internal class CommaSeparatedIntegerCollectionModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null)
{
return false;
}
string s = val.AttemptedValue;
if (s == null || s.IndexOf(",", StringComparison.Ordinal) == 0)
{
bindingContext.Model = new int[] { };
}
var stringArray = s.Split(new[] { "," }, StringSplitOptions.None);
var listInt = new List<int>(stringArray.Count());
int valueInt;
foreach (string valueString in stringArray)
{
if (!int.TryParse(valueString, out valueInt))
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Values are not numeric");
return false;
}
listInt.Add(valueInt);
}
bindingContext.Model = listInt;
return true;
}
}
Is there some way I can instruct the model binder how to bind this kind of properties?

Call WebApi action with a list of parameters [duplicate]

I have an ASP.NET Web API (version 4) REST service where I need to pass an array of integers.
Here is my action method:
public IEnumerable<Category> GetCategories(int[] categoryIds){
// code to retrieve categories from database
}
And this is the URL that I have tried:
/Categories?categoryids=1,2,3,4
You just need to add [FromUri] before parameter, looks like:
GetCategories([FromUri] int[] categoryIds)
And send request:
/Categories?categoryids=1&categoryids=2&categoryids=3
As Filip W points out, you might have to resort to a custom model binder like this (modified to bind to actual type of param):
public IEnumerable<Category> GetCategories([ModelBinder(typeof(CommaDelimitedArrayModelBinder))]long[] categoryIds)
{
// do your thing
}
public class CommaDelimitedArrayModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var key = bindingContext.ModelName;
var val = bindingContext.ValueProvider.GetValue(key);
if (val != null)
{
var s = val.AttemptedValue;
if (s != null)
{
var elementType = bindingContext.ModelType.GetElementType();
var converter = TypeDescriptor.GetConverter(elementType);
var values = Array.ConvertAll(s.Split(new[] { ","},StringSplitOptions.RemoveEmptyEntries),
x => { return converter.ConvertFromString(x != null ? x.Trim() : x); });
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Model = typedValues;
}
else
{
// change this line to null if you prefer nulls to empty arrays
bindingContext.Model = Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0);
}
return true;
}
return false;
}
}
And then you can say:
/Categories?categoryids=1,2,3,4 and ASP.NET Web API will correctly bind your categoryIds array.
I recently came across this requirement myself, and I decided to implement an ActionFilter to handle this.
public class ArrayInputAttribute : ActionFilterAttribute
{
private readonly string _parameterName;
public ArrayInputAttribute(string parameterName)
{
_parameterName = parameterName;
Separator = ',';
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsKey(_parameterName))
{
string parameters = string.Empty;
if (actionContext.ControllerContext.RouteData.Values.ContainsKey(_parameterName))
parameters = (string) actionContext.ControllerContext.RouteData.Values[_parameterName];
else if (actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName] != null)
parameters = actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName];
actionContext.ActionArguments[_parameterName] = parameters.Split(Separator).Select(int.Parse).ToArray();
}
}
public char Separator { get; set; }
}
I am applying it like so (note that I used 'id', not 'ids', as that is how it is specified in my route):
[ArrayInput("id", Separator = ';')]
public IEnumerable<Measure> Get(int[] id)
{
return id.Select(i => GetData(i));
}
And the public url would be:
/api/Data/1;2;3;4
You may have to refactor this to meet your specific needs.
In case someone would need - to achieve same or similar thing(like delete) via POST instead of FromUri, use FromBody and on client side(JS/jQuery) format param as $.param({ '': categoryids }, true)
c#:
public IHttpActionResult Remove([FromBody] int[] categoryIds)
jQuery:
$.ajax({
type: 'POST',
data: $.param({ '': categoryids }, true),
url: url,
//...
});
The thing with $.param({ '': categoryids }, true) is that it .net will expect post body to contain urlencoded value like =1&=2&=3 without parameter name, and without brackets.
Easy way to send array params to web api
API
public IEnumerable<Category> GetCategories([FromUri]int[] categoryIds){
// code to retrieve categories from database
}
Jquery : send JSON object as request params
$.get('api/categories/GetCategories',{categoryIds:[1,2,3,4]}).done(function(response){
console.log(response);
//success response
});
It will generate your request URL like
../api/categories/GetCategories?categoryIds=1&categoryIds=2&categoryIds=3&categoryIds=4
You may try this code for you to take comma separated values / an array of values to get back a JSON from webAPI
public class CategoryController : ApiController
{
public List<Category> Get(String categoryIDs)
{
List<Category> categoryRepo = new List<Category>();
String[] idRepo = categoryIDs.Split(',');
foreach (var id in idRepo)
{
categoryRepo.Add(new Category()
{
CategoryID = id,
CategoryName = String.Format("Category_{0}", id)
});
}
return categoryRepo;
}
}
public class Category
{
public String CategoryID { get; set; }
public String CategoryName { get; set; }
}
Output :
[
{"CategoryID":"4","CategoryName":"Category_4"},
{"CategoryID":"5","CategoryName":"Category_5"},
{"CategoryID":"3","CategoryName":"Category_3"}
]
ASP.NET Core 2.0 Solution (Swagger Ready)
Input
DELETE /api/items/1,2
DELETE /api/items/1
Code
Write the provider (how MVC knows what binder to use)
public class CustomBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(int[]) || context.Metadata.ModelType == typeof(List<int>))
{
return new BinderTypeModelBinder(typeof(CommaDelimitedArrayParameterBinder));
}
return null;
}
}
Write the actual binder (access all sorts of info about the request, action, models, types, whatever)
public class CommaDelimitedArrayParameterBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ActionContext.RouteData.Values[bindingContext.FieldName] as string;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
var ints = value?.Split(',').Select(int.Parse).ToArray();
bindingContext.Result = ModelBindingResult.Success(ints);
if(bindingContext.ModelType == typeof(List<int>))
{
bindingContext.Result = ModelBindingResult.Success(ints.ToList());
}
return Task.CompletedTask;
}
}
Register it with MVC
services.AddMvc(options =>
{
// add custom binder to beginning of collection
options.ModelBinderProviders.Insert(0, new CustomBinderProvider());
});
Sample usage with a well documented controller for Swagger
/// <summary>
/// Deletes a list of items.
/// </summary>
/// <param name="itemIds">The list of unique identifiers for the items.</param>
/// <returns>The deleted item.</returns>
/// <response code="201">The item was successfully deleted.</response>
/// <response code="400">The item is invalid.</response>
[HttpDelete("{itemIds}", Name = ItemControllerRoute.DeleteItems)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
public async Task Delete(List<int> itemIds)
=> await _itemAppService.RemoveRangeAsync(itemIds);
EDIT: Microsoft recommends using a TypeConverter for these kids of operations over this approach. So follow the below posters advice and document your custom type with a SchemaFilter.
Instead of using a custom ModelBinder, you can also use a custom type with a TypeConverter.
[TypeConverter(typeof(StrListConverter))]
public class StrList : List<string>
{
public StrList(IEnumerable<string> collection) : base(collection) {}
}
public class StrListConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value == null)
return null;
if (value is string s)
{
if (string.IsNullOrEmpty(s))
return null;
return new StrList(s.Split(','));
}
return base.ConvertFrom(context, culture, value);
}
}
The advantage is that it makes the Web API method's parameters very simple. You dont't even need to specify [FromUri].
public IEnumerable<Category> GetCategories(StrList categoryIds) {
// code to retrieve categories from database
}
This example is for a List of strings, but you could do categoryIds.Select(int.Parse) or simply write an IntList instead.
I originally used the solution that #Mrchief for years (it works great). But when when I added Swagger to my project for API documentation my end point was NOT showing up.
It took me a while, but this is what I came up with. It works with Swagger, and your API method signatures look cleaner:
In the end you can do:
// GET: /api/values/1,2,3,4
[Route("api/values/{ids}")]
public IHttpActionResult GetIds(int[] ids)
{
return Ok(ids);
}
WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Allow WebApi to Use a Custom Parameter Binding
config.ParameterBindingRules.Add(descriptor => descriptor.ParameterType == typeof(int[]) && descriptor.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get)
? new CommaDelimitedArrayParameterBinder(descriptor)
: null);
// Allow ApiExplorer to understand this type (Swagger uses ApiExplorer under the hood)
TypeDescriptor.AddAttributes(typeof(int[]), new TypeConverterAttribute(typeof(StringToIntArrayConverter)));
// Any existing Code ..
}
}
Create a new class: CommaDelimitedArrayParameterBinder.cs
public class CommaDelimitedArrayParameterBinder : HttpParameterBinding, IValueProviderParameterBinding
{
public CommaDelimitedArrayParameterBinder(HttpParameterDescriptor desc)
: base(desc)
{
}
/// <summary>
/// Handles Binding (Converts a comma delimited string into an array of integers)
/// </summary>
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
var queryString = actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName] as string;
var ints = queryString?.Split(',').Select(int.Parse).ToArray();
SetValue(actionContext, ints);
return Task.CompletedTask;
}
public IEnumerable<ValueProviderFactory> ValueProviderFactories { get; } = new[] { new QueryStringValueProviderFactory() };
}
Create a new class: StringToIntArrayConverter.cs
public class StringToIntArrayConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
}
Notes:
https://stackoverflow.com/a/47123965/862011 pointed me in the right direction
Swagger was only failing to pick my comma delimited end points when using the [Route] attribute
public class ArrayInputAttribute : ActionFilterAttribute
{
private readonly string[] _ParameterNames;
/// <summary>
///
/// </summary>
public string Separator { get; set; }
/// <summary>
/// cons
/// </summary>
/// <param name="parameterName"></param>
public ArrayInputAttribute(params string[] parameterName)
{
_ParameterNames = parameterName;
Separator = ",";
}
/// <summary>
///
/// </summary>
public void ProcessArrayInput(HttpActionContext actionContext, string parameterName)
{
if (actionContext.ActionArguments.ContainsKey(parameterName))
{
var parameterDescriptor = actionContext.ActionDescriptor.GetParameters().FirstOrDefault(p => p.ParameterName == parameterName);
if (parameterDescriptor != null && parameterDescriptor.ParameterType.IsArray)
{
var type = parameterDescriptor.ParameterType.GetElementType();
var parameters = String.Empty;
if (actionContext.ControllerContext.RouteData.Values.ContainsKey(parameterName))
{
parameters = (string)actionContext.ControllerContext.RouteData.Values[parameterName];
}
else
{
var queryString = actionContext.ControllerContext.Request.RequestUri.ParseQueryString();
if (queryString[parameterName] != null)
{
parameters = queryString[parameterName];
}
}
var values = parameters.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries)
.Select(TypeDescriptor.GetConverter(type).ConvertFromString).ToArray();
var typedValues = Array.CreateInstance(type, values.Length);
values.CopyTo(typedValues, 0);
actionContext.ActionArguments[parameterName] = typedValues;
}
}
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
_ParameterNames.ForEach(parameterName => ProcessArrayInput(actionContext, parameterName));
}
}
Usage:
[HttpDelete]
[ArrayInput("tagIDs")]
[Route("api/v1/files/{fileID}/tags/{tagIDs}")]
public HttpResponseMessage RemoveFileTags(Guid fileID, Guid[] tagIDs)
{
_FileRepository.RemoveFileTags(fileID, tagIDs);
return Request.CreateResponse(HttpStatusCode.OK);
}
Request uri
http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63
If you want to list/ array of integers easiest way to do this is accept the comma(,) separated list of string and convert it to list of integers.Do not forgot to mention [FromUri] attriubte.your url look like:
...?ID=71&accountID=1,2,3,289,56
public HttpResponseMessage test([FromUri]int ID, [FromUri]string accountID)
{
List<int> accountIdList = new List<int>();
string[] arrAccountId = accountId.Split(new char[] { ',' });
for (var i = 0; i < arrAccountId.Length; i++)
{
try
{
accountIdList.Add(Int32.Parse(arrAccountId[i]));
}
catch (Exception)
{
}
}
}
I have created a custom model binder which converts any comma separated values (only primitive, decimal, float, string) to their corresponding arrays.
public class CommaSeparatedToArrayBinder<T> : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Type type = typeof(T);
if (type.IsPrimitive || type == typeof(Decimal) || type == typeof(String) || type == typeof(float))
{
ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (val == null) return false;
string key = val.RawValue as string;
if (key == null) { bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type"); return false; }
string[] values = key.Split(',');
IEnumerable<T> result = this.ConvertToDesiredList(values).ToArray();
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Only primitive, decimal, string and float data types are allowed...");
return false;
}
private IEnumerable<T> ConvertToDesiredArray(string[] values)
{
foreach (string value in values)
{
var val = (T)Convert.ChangeType(value, typeof(T));
yield return val;
}
}
}
And how to use in Controller:
public IHttpActionResult Get([ModelBinder(BinderType = typeof(CommaSeparatedToArrayBinder<int>))] int[] ids)
{
return Ok(ids);
}
Make the method type [HttpPost], create a model that has one int[] parameter, and post with json:
/* Model */
public class CategoryRequestModel
{
public int[] Categories { get; set; }
}
/* WebApi */
[HttpPost]
public HttpResponseMessage GetCategories(CategoryRequestModel model)
{
HttpResponseMessage resp = null;
try
{
var categories = //your code to get categories
resp = Request.CreateResponse(HttpStatusCode.OK, categories);
}
catch(Exception ex)
{
resp = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
}
return resp;
}
/* jQuery */
var ajaxSettings = {
type: 'POST',
url: '/Categories',
data: JSON.serialize({Categories: [1,2,3,4]}),
contentType: 'application/json',
success: function(data, textStatus, jqXHR)
{
//get categories from data
}
};
$.ajax(ajaxSettings);
Or you could just pass a string of delimited items and put it into an array or list on the receiving end.
I addressed this issue this way.
I used a post message to the api to send the list of integers as data.
Then I returned the data as an ienumerable.
The sending code is as follows:
public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
IEnumerable<Contact> result = null;
if (ids!=null&&ids.Count()>0)
{
try
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:49520/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
String _endPoint = "api/" + typeof(Contact).Name + "/ListArray";
HttpResponseMessage response = client.PostAsJsonAsync<IEnumerable<int>>(_endPoint, ids).Result;
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{
result = JsonConvert.DeserializeObject<IEnumerable<Contact>>(response.Content.ReadAsStringAsync().Result);
}
}
}
catch (Exception)
{
}
}
return result;
}
The receiving code is as follows:
// POST api/<controller>
[HttpPost]
[ActionName("ListArray")]
public IEnumerable<Contact> Post([FromBody]IEnumerable<int> ids)
{
IEnumerable<Contact> result = null;
if (ids != null && ids.Count() > 0)
{
return contactRepository.Fill(ids);
}
return result;
}
It works just fine for one record or many records. The fill is an overloaded method using DapperExtensions:
public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
IEnumerable<Contact> result = null;
if (ids != null && ids.Count() > 0)
{
using (IDbConnection dbConnection = ConnectionProvider.OpenConnection())
{
dbConnection.Open();
var predicate = Predicates.Field<Contact>(f => f.id, Operator.Eq, ids);
result = dbConnection.GetList<Contact>(predicate);
dbConnection.Close();
}
}
return result;
}
This allows you to fetch data from a composite table (the id list), and then return the records you are really interested in from the target table.
You could do the same with a view, but this gives you a little more control and flexibility.
In addition, the details of what you are seeking from the database are not shown in the query string. You also do not have to convert from a csv file.
You have to keep in mind when using any tool like the web api 2.x interface is that the get, put, post, delete, head, etc., functions have a general use, but are not restricted to that use.
So, while post is generally used in a create context in the web api interface, it is not restricted to that use. It is a regular html call that can be used for any purpose permitted by html practice.
In addition, the details of what is going on are hidden from those "prying eyes" we hear so much about these days.
The flexibility in naming conventions in the web api 2.x interface and use of regular web calling means you send a call to the web api that misleads snoopers into thinking you are really doing something else. You can use "POST" to really retrieve data, for example.
My solution was to create an attribute to validate strings, it does a bunch of extra common features, including regex validation that you can use to check for numbers only and then later I convert to integers as needed...
This is how you use:
public class MustBeListAndContainAttribute : ValidationAttribute
{
private Regex regex = null;
public bool RemoveDuplicates { get; }
public string Separator { get; }
public int MinimumItems { get; }
public int MaximumItems { get; }
public MustBeListAndContainAttribute(string regexEachItem,
int minimumItems = 1,
int maximumItems = 0,
string separator = ",",
bool removeDuplicates = false) : base()
{
this.MinimumItems = minimumItems;
this.MaximumItems = maximumItems;
this.Separator = separator;
this.RemoveDuplicates = removeDuplicates;
if (!string.IsNullOrEmpty(regexEachItem))
regex = new Regex(regexEachItem, RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.IgnoreCase);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var listOfdValues = (value as List<string>)?[0];
if (string.IsNullOrWhiteSpace(listOfdValues))
{
if (MinimumItems > 0)
return new ValidationResult(this.ErrorMessage);
else
return null;
};
var list = new List<string>();
list.AddRange(listOfdValues.Split(new[] { Separator }, System.StringSplitOptions.RemoveEmptyEntries));
if (RemoveDuplicates) list = list.Distinct().ToList();
var prop = validationContext.ObjectType.GetProperty(validationContext.MemberName);
prop.SetValue(validationContext.ObjectInstance, list);
value = list;
if (regex != null)
if (list.Any(c => string.IsNullOrWhiteSpace(c) || !regex.IsMatch(c)))
return new ValidationResult(this.ErrorMessage);
return null;
}
}
I just added the Query key (Refit lib) in the property for the request.
[Query(CollectionFormat.Multi)]
public class ExampleRequest
{
[FromQuery(Name = "name")]
public string Name { get; set; }
[AliasAs("category")]
[Query(CollectionFormat.Multi)]
public List<string> Categories { get; set; }
}
All other solutions need too much work. I was trying to use IEnumerable<long> or long[] in a HttpGet method parameter, but I see no point of doing all the work just to make the signature of the handler method parameter long[]. I ended up just making it string, and then separated it within the handler. Took me one line.
public async Task<IActionResult> SomeHandler(string idsString)
{
var ids = idsString.Split(',').Select(x => long.Parse(x));
Now you can just pass the numbers like
.../SomeHandler?idsString=123,456,789,012

How to use Mapper.cs with Instasharp

I'm a french student and I have to do a project for the semester.
I have to get medias from Instagram (with the api) and put them in a database. I use instasharp for now and I can do a request with the HttpClient class.
This returns a string with the content of the JSON request. But I've seen a class named Mapper.cs, which should match the result of the request with the different class of instasharp.
For example, if I search for a media, the mapper class should read the JSON string a create an appropriate instance of Media.
I do this kind of request: https://api.instagram.com/v1/media/search?lat=45.759723&lng=4.842223&distance=5000&client_id=My-Id&count=5
But I'm not sure of it, is somebody able to tell me what the Mapper class really does.
Here is the Mapper class, I do not understand all of the method, so I can't use it.
class Mapper {
public static object Map<T>(string json) where T : new() {
//var t = new T();
var j = JObject.Parse(json);
var t = typeof(T);
try {
var instance = Map(t, j);
// add the pure json back
if (instance != null) {
var prop = instance.GetType().GetProperty("Json");
if (prop != null) {
prop.SetValue(instance, json, null);
}
}
return instance;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
return null;
}
}
private static object Map(Type t, JObject json) {
var instance = Activator.CreateInstance(t);
Array.ForEach(instance.GetType().GetProperties(), prop => {
var attribute = prop.GetCustomAttributes(typeof(Model.JsonMapping), false);
if (attribute.Length > 0) {
var propertyType = prop.PropertyType;
var mapsTo = ((Model.JsonMapping)attribute[0]).MapsTo;
var mappingType = ((Model.JsonMapping)attribute[0]).MapType;
switch (mappingType) {
case Model.JsonMapping.MappingType.Class:
if (json[mapsTo] != null) {
if (json[mapsTo].HasValues) {
var obj = Map(propertyType, (JObject)json[mapsTo]);
prop.SetValue(instance, obj, null);
}
}
break;
case Model.JsonMapping.MappingType.Collection:
var col = Map(propertyType, (JArray)json[mapsTo]);
prop.SetValue(instance, col, null);
break;
default:
if (json != null) {
if (json[mapsTo] != null) {
// special case for datetime because it comes in Unix format
if (prop.PropertyType == typeof(DateTime))
prop.SetValue(instance, UnixTimeStampToDateTime(json[mapsTo].ToString()), null);
else
prop.SetValue(instance, Convert.ChangeType(json[mapsTo].ToString(), prop.PropertyType), null);
}
}
break;
}
}
});
return instance;
}
private static IList Map(Type t, JArray json) {
var type = t.GetGenericArguments()[0];
// This will produce List<Image> or whatever the original element type is
var listType = typeof(List<>).MakeGenericType(type);
var result = (IList)Activator.CreateInstance(listType);
if (json != null) {
foreach (var j in json)
if (type.Name == "String" || type.Name == "Int32")
result.Add(j.ToString());
else result.Add(Map(type, (JObject)j));
}
return result;
}
private static DateTime UnixTimeStampToDateTime(string unixTimeStamp) {
// Unix timestamp is seconds past epoch
double unixTime = Convert.ToDouble(unixTimeStamp);
System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
dtDateTime = dtDateTime.AddSeconds(unixTime).ToLocalTime();
return dtDateTime;
}
private static void SetPropertyValue(PropertyInfo prop, object instance, object value) {
prop.SetValue(instance, Convert.ChangeType(value, prop.PropertyType), null);
}
}
And that is what I tried, but it doesn't work, "Erreur 540 'InstaSharp.Mapper.Map(string)' is not accessible" (something like this, I translated the error myself)
And this one "Error 539 The type or namespace name 'media' is not found ( a using directive or an assembly reference is it missing ?"
InstaSharp.Model.Media media = new InstaSharp.Model.Media();
InstaSharp.Mapper.Map<media>(reponse);
I really don't know how to use the Mapper class because I never saw a class like that (I started to use C# only 6 month ago)
The Map<T> method is expecting a type, so the type of media, not the instance media. Your usage should be something like:
InstaSharp.Mapper.Map<InstaSharp.Model.Media>(response);
As for what the class does, it exposes one static method Map<T>(string) and looks like it uses JSON.net to deserialize the string parameter into an instance of the type T with data values mapped from the JSON.
If you wanted a List<T> of Media, you'd do much the same thing as before:
InstaSharp.Mapper.Map<List<InstaSharp.Model.Media>>(response);
Lastly, because the result of Map<T> is an object, you'll need to cast it or use a dynamic variable to actually do anything.
var x = (List<Model.Media>)Mapper.Map<List<Model.Media>>(response);
To specify what class properties map to what json property, you'll need to add the Model.JsonMapping attribute to your properties and specify the MapTo attribute option.
[Model.JsonMapping(MapTo="someJsonProperty")]
public string SomeProperty {get;set;}

MVC 3 doesn't bind nullable long

I made a test website to debug an issue I'm having, and it appears that either I'm passing in the JSON data wrong or MVC just can't bind nullable longs. I'm using the latest MVC 3 release, of course.
public class GetDataModel
{
public string TestString { get; set; }
public long? TestLong { get; set; }
public int? TestInt { get; set; }
}
[HttpPost]
public ActionResult GetData(GetDataModel model)
{
// Do stuff
}
I'm posting a JSON string with the correct JSON content type:
{ "TestString":"test", "TestLong":12345, "TestInt":123 }
The long isn't bound, it's always null. It works if I put the value in quotes, but I shouldn't have to do that, should I? Do I need to have a custom model binder for that value?
I created a testproject just to test this. I put your code into my HomeController and added this to index.cshtml:
<script type="text/javascript">
$(function () {
$.post('Home/GetData', { "TestString": "test", "TestLong": 12345, "TestInt": 123 });
});
</script>
I put a breakpoint in the GetData method, and the values were binded to the model like they should:
So I think there's something wrong with the way you send the values. Are you sure the "TestLong" value is actually sent over the wire? You can check this using Fiddler.
If you don't want to go with Regex and you only care about fixing long?, the following will also fix the problem:
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var provider = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (provider != null
&& provider.RawValue != null
&& Type.GetTypeCode(provider.RawValue.GetType()) == TypeCode.Int32)
{
var value = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize(provider.AttemptedValue, bindingContext.ModelMetadata.ModelType);
return value;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
My colleague came up with a workaround for this. The solution is to take the input stream and use a Regex to wrap all numeric variables in quotes to trick the JavaScriptSerializer into deserialising the longs properly. It's not a perfect solution, but it takes care of the issue.
This is done in a custom model binder. I used Posting JSON Data to ASP.NET MVC as an example. You have to take care, though, if the input stream is accessed anywhere else.
public class JsonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!IsJSONRequest(controllerContext))
return base.BindModel(controllerContext, bindingContext);
// Get the JSON data that's been posted
var jsonStringData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
// Wrap numerics
jsonStringData = Regex.Replace(jsonStringData, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
Then put this in the Global:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Now the long gets bound successfully. I would call this a bug in the JavaScriptSerializer. Also note that arrays of longs or nullable longs get bound just fine without the quotes.
You can use this model binder class
public class LongModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (string.IsNullOrEmpty(valueResult.AttemptedValue))
{
return (long?)null;
}
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToInt64(
valueResult.AttemptedValue,
CultureInfo.InvariantCulture
);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
In Global.asax Application_Start add these lines
ModelBinders.Binders.Add(typeof(long), new LongModelBinder());
ModelBinders.Binders.Add(typeof(long?), new LongModelBinder());
I wanted to incorporate the solution presented by Edgar but still have the features of the DefaultModelBinder. So instead of creating a new model binder I went with a different approach and replaced the JsonValueProviderFactory with a custom one. There's only a minor change in the code from the original MVC3 source code:
public sealed class NumericJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
// below is the code that Edgar proposed and the only change to original source code
bodyText = Regex.Replace(bodyText, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
Then to register the new value provider you need to add the following lines to your Global.asax:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new NumericJsonValueProviderFactory());

Categories