.NET CORE WEB API accept list of integers as an input param in HTTP GET API - asp.net-core

I am using .net core 3+ web api.
Below is how my action looks like below, it uses HTTP GET and I want to pass few fields and one of the fields is a list of integers.
[HttpGet]
[Route("cities")]
public ActionResult<IEnumerable<City>> GetCities([FromQuery] CityQuery query)
{...}
and here is CityQuery class -
public class CityQuery
{
[FromQuery(Name = "stateids")]
[Required(ErrorMessage = "stateid is missing")]
public string StateIdsStr { get; set; }
public IEnumerable<int> StateList
{
get
{
if (!string.IsNullOrEmpty(StateIdsStr))
{
var output = StateIdsStr.Split(',').Select(id =>
{
int.TryParse(id, out var stateId);
return stateId;
}).ToList();
return output;
}
return new List<int>();
}
}
}
Is there a generic way I can use to accept list of integers as input and not accept string and then parse it?
Or is there a better way to do this? I tried googling but could not find much. Thanks in advance.

This can help
[HttpGet]
[Route("cities")]
public ActionResult<IEnumerable<City>> GetCities([FromQuery] int[] stateids)
{
...
}
but the query string will change to
https://localhost/api/controller/cities?stateids=1&stateids=2&stateids=3
If you required comma separated query string with integer, you can go for Custom model binder
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-3.1

You can use custom model binding, below is a working demo:
Model:
public class CityQuery
{
public List<int> StateList{ get; set; }
}
CustomModelBinder:
public class CustomModelBinder: IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var values = bindingContext.ValueProvider.GetValue("stateids");
if (values.Length == 0)
{
return Task.CompletedTask;
}
var splitData = values.FirstValue.Split(',');
var result = new CityQuery()
{
StateList = new List<int>()
};
foreach(var id in splitData)
{
result.StateList.Add(int.Parse(id));
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Applying ModelBinding Attribute on Action method:
[HttpGet]
[Route("cities")]
public ActionResult GetCities([ModelBinder(BinderType = typeof(CustomModelBinder))] CityQuery query)
{
return View();
}
when the url like /cities?stateids=1,2,3, the stateids will be filled to StateList

I think you just need to use [FromUri] before int array parameter :
public ActionResult<IEnumerable<City>> GetCities([FromUri] int[] stateList)
And request would be like :
/cities?stateList=1&stateList=2&stateList=3

Related

How do you manage the visible input fields accepted in an API HttpPost request?

In my API I have a Create method in my controller that accepts all of the models fields, but in the method I'm excluding the ID field since on a create it's generated. But in Swagger it's showing the following.
Is there a way for it not to show the following part?
"id": 0
Is a viewmodel how I should go about this?
I tried the following, but can't get it to work.
public class PartVM
{
public string Name { get; set; }
}
public interface IPartService
{
Task<Part> CreatePart(PartVM part);
Task<IEnumerable<Part>> GetParts();
Task<Part> GetPart(int partId);
}
public class PartService : IPartService
{
private readonly AppDbContext _appDbContext;
public PartService(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
public async Task<Part> CreatePart(PartVM part)
{
var _part = new Part()
{
Name = part.Name
};
var result = await _appDbContext.Parts.AddAsync(_part);
await _appDbContext.SaveChangesAsync();
return result.Entity;
}
}
Here's my controller.
[Route("api/[controller]")]
[ApiController]
public class PartsController : ControllerBase
{
private readonly IPartService _partService;
public PartsController(IPartService partService)
{
_partService = partService;
}
[HttpPost]
public async Task<ActionResult<Part>> CreatePart(PartVM part)
{
try
{
if (part == null)
return BadRequest();
var _part = new Part()
{
Name = part.Name
};
var createdPart = await _partService.CreatePart(_part);
return CreatedAtAction(nameof(GetPart),
new { id = createdPart.Id}, createdPart);
}
catch (Exception /*ex*/)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error creating new record in the database");
}
}
I'm getting a build error saying "CS1503 Argument 1: cannot convert from 'MusicManager.Shared.Part' to 'MusicManager.Server.Data.ViewModels.PartVM'".
It's refering to "_part" in this line "var createdPart = await _partService.CreatePart(_part);".
Any help is appreciated, thank you!
you have a CreatePart method which receives a PartVM model, but you are sending a Part Model to it
change your method to this :
public async Task<Part> CreatePart(Part part)
{
var result = await _appDbContext.Parts.AddAsync(_part);
await _appDbContext.SaveChangesAsync();
return result.Entity;
}

Receiving multipart form-data json parameter null

I am trying to receive a multipart request from Postman containg 3 parameters:
An int
A file
A Json
I receive in the controller both the file and the integer fine, but the json has all the fields as null.
What could be wrong ?
Json
[Serializable]
public class ProcessingRecipe
{
[JsonPropertyName("fileId")]
public string FileID { get; set; }
[JsonPropertyName("srcLang")]
public string SrcLang { get; set; }
}
Controller
[HttpPost]
[Route(Routes.Routes.File.PROCESS)]
public async Task<ActionResult<FileProcessResponse>> ProcessFileAsync([FromForm]IFormFile uploadFile,[FromForm] ProcessingRecipe recipe,[FromForm]int aa)
{
//the file is ok
// the int is 33
}
Postman
Update !!!!!
I have used according to this post to no avail:
Custom Binder
public class JsonModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException(nameof(bindingContext));
}
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None) {
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = Newtonsoft.Json.JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType);
if (result != null) {
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Controller action
public async Task<ActionResult<FileProcessResponse>> ProcessFileAsync([FromForm]IFormFile uploadFile,[ModelBinder(typeof(JsonModelBinder))] ProcessingRecipe recipe)
{
//the file is ok
// the int is 33
}
This is a definite duplicate of how to upload a file and json data in postman
In your case, maybe you can try something like what is suggested in the following(https://stackoverflow.com/a/52748531/11226302)

How to send object which contains IEnumerable via Refit on NetCore?

I have to send a request object via Refit which contains 2 IEnumerable and one string, but for some reason I can't send the object forward.
I've tried to use all the paramets from the interface. Ex: [Query(CollectionFormat.Csv)] or Multi / Pipes but no success.
I've also tried to create my own CustomUrlParameterFormatter but unfortunately here I'm stuck, because I don't see a good way to retrieve the name of the property from the object request that I'm sending.
The code for CustomUrlParameterFormatter
public class CustomUrlParameterFormatter : IUrlParameterFormatter
{
public string Format(object value, ParameterInfo parameterInfo)
{
if(value is IEnumerable enumerable)
{
var result = ToQueryString(enumerable, parameterInfo.Name);
return result;
}
return string.Empty;
}
public static string ToQueryString(IEnumerable query, string parameterName)
{
var values = query.Cast<object>().Select(ToString).ToArray();
var separator = parameterName + "=";
return values.Any() ? separator + string.Join("&" + separator, values) : "";
}
public static string ToString(object value)
{
var json = JsonConvert.SerializeObject(value).Replace("\\\"", "\"").Trim('"');
return Uri.EscapeUriString(json);
}
}
The Call from the IService that I'm using
[Get("/TestMethod")]
Task<HttpResponseMessage> TestMethod([Query]TestRequestDTO requestDTO, [Header("X-Correlation-ID")] string correlationId);
The Request object
public class TestRequestDTO
{
public IEnumerable<long> EnumOne { get; set; }
public IEnumerable<long> EnumTwo { get; set; }
public string MethodString { get; set; }
}
Also the RefitClient configuration
var refitSettings = new RefitSettings();
refitSettings.UrlParameterFormatter = new CustomUrlParameterFormatter();
services.AddRefitClient<IService>(refitSettings)
.ConfigureHttpClient(c => c.BaseAddress = new Uri(settings.Services.IService));
What I'm trying to achieve is something like
TestMethod?EnumOne =123&EnumOne =321&EnumTwo=123&EnumTwo=321&methodString=asdsaa
and instead I'm receiving other behavior
without CustomUrlParameterFormatter()
TestMethod?EnumOne=System.Collections.Generic.List`1%5BSystem.Int64%5D&EnumTwo=System.Collections.Generic.List`1%5BSystem.Int64%5D&MethodString=sdf

Setting up examples in Swagger

I am using Swashbuckle.AspNetCore.Swagger (1.0.0) and Swashbuckle.AspNetCore.SwaggerGen (1.0.0). I am trying to add default examples to my API following Default model example in Swashbuckle (Swagger). I created a new class file and added,
public class SwaggerDefaultValue : Attribute
{
public string ParameterName { get; set; }
public string Value { get; set; }
public SwaggerDefaultValue(string parameterName, string value)
{
this.ParameterName = parameterName;
this.Value = value;
}
}
public class AddDefaultValues : IOperationFilter
{
public void Apply(Operation operation, DataTypeRegistry dataTypeRegistry, ApiDescription apiDescription)
{
foreach (var param in operation.Parameters)
{
var actionParam = apiDescription.ActionDescriptor.GetParameters().First(p => p.ParameterName == param.Name);
if (actionParam != null)
{
var customAttribute = actionParam.ActionDescriptor.GetCustomAttributes<SwaggerDefaultValue>().FirstOrDefault();
if (customAttribute != null)
{
param.DefaultValue = customAttribute.Value;
}
}
}
}
}
but I get this error - AddDefaultValues does not implement interface member IOperationFilter.Apply(Operation, OperationFilterContext)
That link you are following is not for the Swashbuckle.AspNetCore version
Look in the correct project for the proper examples:
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/search?q=IOperationFilter&unscoped_q=IOperationFilter

Restore AJAX handling for ASP.NET Core to previous functionality

In previous MVC5 and below, you could make an ajax call that unwrapped the parameters properly:
JS:
$.post('/controller/endpoint',{intparam: 1, strparam: 'hello'})
CS:
public ActionResult endpoint(int intparam, string strparam){}
In the new aspnetcore, it has changed:
CS:
public CustomClassWrapper{
public int intparam {get;set;}
public string stringparam {get;set;}
}
public ActionResult endpoint([FromBody]CustomClassWrapper item){}
To sum it up, in the new framework, you need to write a wrapper class and can only pass one [FromBody] parameter to the method. Previously, the params would be unwrapped by variable name correctly.
So, i'm trying to re-implement this functionality in an aspnetcore middleware component. I'm having difficulty in how to accomplish calling the controller method properly with the parameters.
My current cut-down code:
public async Task Invoke(HttpContext context)
{
if (IsAjaxRequest(context.Request))
{
try
{
string bodyContent = new StreamReader(context.Request.Body).ReadToEnd();
var parameters = JsonConvert.DeserializeObject(bodyContent);
///What to do here?
}
catch (Exception ex)
{
throw new Exception("AJAX method not found ", ex);
}
}
else
{
await _next(context);
}
}
I'm really just not sure about what to do after deserializing the parameters. I have the URL for the endpoint and also the params correctly. Just need to know how to call the method and return the result as JSON. Should i be using Reflection to get the controller method? Or is there a better way using MVC?
Try implement custom IModelBinder.
public class BodyFieldModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.HttpContext.Request.EnableRewind(); // required to read request body multiple times
var inputStream = bindingContext.HttpContext.Request.Body;
if (inputStream.Position != 0L)
inputStream.Position = 0;
var bodyValue = new StreamReader(inputStream, Encoding.UTF8).ReadToEnd();
var jsonObject = (JObject)JsonConvert.DeserializeObject<object>(bodyValue);
if (jsonObject.TryGetValue(bindingContext.FieldName, out var jToken))
{
var jsonSerializer = JsonSerializer.Create();
var result = jToken.ToObject(bindingContext.ModelType, jsonSerializer);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
}
Be careful, the code above lacks error handling and etc.
And use it like this:
[HttpPost]
public IActionResult Endpoint([ModelBinder(typeof(BodyFieldModelBinder))] int intparam)
Also you could implement custom attribute to reduce complexity of declaration:
public class BodyFieldAttribute : ModelBinderAttribute
{
public BodyFieldAttribute()
: base(typeof(BodyFieldModelBinder))
{
}
}
it's very simple thing i don't know why it not working at your end
JS
$.post('actionMethodURl', { FirstName: '1', LastName: 'hello' }).done(Successfunction);
CS
[HttpPost]
public ActionResult endpoint(string FirstName,string LastName)
{
object Message = string.Empty;
if (ModelState.IsValid)
{
Message = "Pass";
}
else
{
Message = ModelState.Errors();
}
return Json(Message);
}