I currently have a controller with an action that fetches some information, this action receives as a query param a jagged array to filter the information, example: [["Day", ">=", "01.01.2021"],["User", "=", "SomeUserId"]].
My current action declarion:
[HttpGet]
public async Task<object> Get(string[][] filters)
{
...
}
When I make an AJAX request from the client the URL goes encoded in the following way (DevTools request header parameters view):
filters[0][]: Day
filters[0][]: >=
filters[0][]: 01.07.2021
filters[1][]: User
filters[1][]: =
filters[1][]: SomeUserId
Url Encoded FYI: ...filters%5B0%5D%5B%5D=Day&filters%5B0%5D%5B%5D=%3E%3D&filters%5B0%5D%5B%5D=01.07.2021&filters%5B1%5D%5B%5D=User&filters%5B1%5D%5B%5D=%3D&filters%5B1%5D%5B%5D=SomeUserId
The problem
My action when receives the information above has the value of two empty string arrays string[2][]. The following image is a debug print screen from VS of the filters variable.
Should I serialize? Or use a different structure?
Here is a working demo:
View:
<button type="button" onclick="SendRequest()">Click</button>
#section Scripts
{
<script>
function SendRequest() {
var day = "Day";
var simble = encodeURIComponent(">=");
var date = "01.07.2021";
var user = "UserName";
var simble2 = "=";
var id = "1";
$.ajax({
//url: "api/values?filters[0][0]=Day&filters[0][1]=>=&filters[0][2]=01.07.2021&filters[1][0]=User&filters[1][1]==&filters[1][2]=2"
url: "api/values?filters[0][0]=" + day + "&filters[0][1]=" + simble + "&filters[0][2]=" + date + " &filters[1][0]=" + user + "&filters[1][1]=" + simble2 + "&filters[1][2]=" + id + "",
type: 'GET',
success: function (res) {
alert("success");
},
error: function () {
}
})
}
</script>
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
public void Get([FromQuery] string[][] filters)
{
}
}
One option is to wrap the input in a class like
public class InputRequest
{
public string Field { get; set; }
public string Operator { get; set; }
public string Value { get; set; }
}
Then use a custom model binder -
class InputRequestModelBinder : IModelBinder
{
static readonly JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(List<InputRequest>)) return Task.CompletedTask;
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var results = valueProviderResult.Select(t => JsonSerializer.Deserialize<InputRequest>(t, options)).ToList();
bindingContext.Result = ModelBindingResult.Success(results);
return Task.CompletedTask;
}
}
Lastly, hook the model binder to controller method -
[HttpGet]
[Route("show")]
public string Show([ModelBinder(typeof(InputRequestModelBinder))] List<InputRequest> req)
{
//do something
}
Example usage -
/SomeController/show?req=%7B%0A%20%20%22field%22%3A%20%22name%22%2C%0A%20%20%22operator%22%3A%20%22%3D%22%2C%0A%20%20%22value%22%3A%20%22tom%22%0A%7D&req=%7B%0A%20%20%22field%22%3A%20%22age%22%2C%0A%20%20%22operator%22%3A%20%22%3C%22%2C%0A%20%20%22value%22%3A%20%2220%22%0A%7D
Decoded url looks like this -
/SomeController/show?req={
"field": "name",
"operator": "=",
"value": "tom"
}&req={
"field": "age",
"operator": "<",
"value": "20"
}
Pro -
You get to explicitly state the contract to your API consumers about expected values in InputRequest
Con -
Your request query string length increases, so you may hit the limit soon. Although you may convert it to a POST method, but then it violates REST principles.
Related
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;
}
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
I have a class that should represent a controller's action parameter and I'd like its properties to be "required" (meaning, you get a status code 400 or something in case it's passed as null). I managed to get it done using System.ComponentModel.DataAnnotations, but the ErrorMessage that I pass to the constructor of the Required attribute is never shown.
[XmlRoot(ElementName = "root")]
public class Request
{
[XmlElement(ElementName = "prop")]
[Required(ErrorMessage = "The property is required.")]
public string Property { get; set; }
[XmlElement(ElementName = "another")]
[Required(ErrorMessage = "The property is required.")]
public string Another { get; set; }
}
Action:
[HttpPost]
public IActionResult Post([FromBody] Request value)
{
return Ok(value); //ignore this, it's just for testing purposes...
}
However, if I don't pass the Property value, I get a 400 that doesn't contain the ErrorMessage I passed earlier. Am I missing something here?
<ValidationProblemDetails xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>One or more validation errors occurred.</Title>
<Status>400</Status>
</ValidationProblemDetails>
My Startup has Xml formatters added to it:
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.InputFormatters.Insert(0, new XmlSerializerInputFormatter(options));
options.OutputFormatters.Insert(0, new XmlSerializerOutputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
The body of the request looks like this, and it doesn't have "Property":
<root>
<another>Test</another>
<!-- Property "Property" is missing here -->
</root>
Kudos to Code Rethinked for the huge help - Customizing automatic HTTP 400 error response in ASP.NET Core Web APIs.
An approach that I managed to figure out eventually includes the use of services.Configure in my Startup.ConfigureServices method.
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
return new OkObjectResult(new CustomResponse(someStatusCode, context))
{
ContentTypes = { "application/xml" }
};
};
});
So, I made a class named CustomResponse that holds the status code I want to retrieve and all the validation errors (including the ones where my Required property was not passed to the API).
[XmlRoot(ElementName = "rcemsTrxSubReqAck")]
public class CustomResponse
{
[XmlElement(ElementName = "Status")]
public string Status { get; set; }
[XmlArray(ElementName = "Errors"), XmlArrayItem(ElementName = "Error")]
public string[] Errors { get; set; }
public CustomResponse(int status, ActionContext context)
{
Status = status;
Errors = ConstructErrorMessages(context);
}
private string[] ConstructErrorMessages(ActionContext context)
{
if (context == null)
{
return null;
}
string[] arr = new string[context.ModelState.ErrorCount];
int i = 0;
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(errors[0]);
arr[i] = $"{key}: {errorMessage}";
}
else
{
var errorMessages = new string[errors.Count];
for (var j = 0; j < errors.Count; j++)
{
errorMessages[j] = GetErrorMessage(errors[j]);
}
arr[i] = $"{key}: {errorMessages.ToString()}";
}
i++;
}
}
return arr;
}
private string GetErrorMessage(ModelError error)
{
return string.IsNullOrEmpty(error.ErrorMessage) ? "The input was not valid." : error.ErrorMessage;
}
}
I'm having hectic last couple of days due to this problem. I'm trying to pass route data to the view as a matter of navigation. However routeInfo contains no route information. i.e. routeInfo.RouteData.Values.Count = 0. I have another application with the same code which is working fine.
I'm not sure what i'm missing here.
Any help would be really appreciated!!
public ActionResult Index(int type)
{
UrlHelper u = new UrlHelper(this.ControllerContext.RequestContext);
string url = u.Action("Action", "Controller", new { type = type }, Request.Url.Scheme);
Uri uri = new Uri(url);
RouteInfo routeInfo = new RouteInfo(uri, HttpContext.Request.ApplicationPath);
Session["_ReturnURL"] = routeInfo.RouteData.Values;
ViewBag.ReturnURL = Helpers.GetSessionKey("_ReturnURL");
return View();
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.RouteExistingFiles = true;
routes.IgnoreRoute("elmah.axd");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Controller",
url: "Controller/{type}/{action}/{id}",
defaults: new { controller = "Controller", action = "Action", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "CBlah", action = "ABlah", id = UrlParameter.Optional, returnUrl = "~/Blah2/Blah2" }
);
}
public class RouteInfo
{
public RouteData RouteData { get; private set; }
public RouteInfo(RouteData data)
{
RouteData = data;
}
public RouteInfo(Uri uri, string applicationPath)
{
RouteData = RouteTable.Routes.GetRouteData(new InternalHttpContext(uri, applicationPath));
}
private class InternalHttpContext : HttpContextBase
{
private readonly HttpRequestBase _request;
public InternalHttpContext(Uri uri, string applicationPath)
{
_request = new InternalRequestContext(uri, applicationPath);
}
public override HttpRequestBase Request { get { return _request; } }
}
private class InternalRequestContext : HttpRequestBase
{
private readonly string _appRelativePath;
private readonly string _pathInfo;
public InternalRequestContext(Uri uri, string applicationPath)
{
_pathInfo = uri.Query;
if (String.IsNullOrEmpty(applicationPath) || !uri.AbsolutePath.StartsWith(applicationPath, StringComparison.OrdinalIgnoreCase))
_appRelativePath = uri.AbsolutePath.Substring(applicationPath.Length);
else
_appRelativePath = uri.AbsolutePath;
}
public override string AppRelativeCurrentExecutionFilePath { get { return String.Concat("~", _appRelativePath); } }
public override string PathInfo { get { return _pathInfo; } }
}
}
Route.GetRouteData Definition
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults);
if (values == null)
{
return null;
}
RouteData data = new RouteData(this, this.RouteHandler);
if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest))
{
return null;
}
foreach (KeyValuePair<string, object> pair in values)
{
data.Values.Add(pair.Key, pair.Value);
}
if (this.DataTokens != null)
{
foreach (KeyValuePair<string, object> pair2 in this.DataTokens)
{
data.DataTokens[pair2.Key] = pair2.Value;
}
}
return data;
}
The problem lies in this line:
string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
You are appending a query string as the httpContext.Request.PathInfo via:
_pathInfo = uri.Query;
I know for a fact that the Route class does not consider the query string, so appending it to the path is a mistake. There is also some information here that backs up the fact that PathInfo always should return an empty string.
Per MSDN:
For the URL http://www.contoso.com/virdir/page.html/tail, the PathInfo value is /tail.
So, adding a query string is making this line fail (when there is a query string) because _parsedRoute.Match isn't expecting one.
RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults);
My guess is that one of your applications "works" because it is not being passed a URL with a query string, and therefore matches correctly. But whatever the case, you should return an empty string from PathInfo in your InternalRequestContext class to make it work 100% of the time (unless you have crazy URLs with dots in them, then you may need to do some extra work).
I want that action was available only to performance at the request of object of XHR. As I tried it to realize:
In the controller there is an action:
public string Act()
{
string view="";
if(Request.Headers["p"]!="p")
Response.Redirect("/",true);
else
view = GetActView();
return view;
}
It is caused by means of onclick of an event to which function is attached:
function updateDiv() {
xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
document.getElementById('actdiv').innerHTML = xmlhttp.responseText;
}
}
xmlhttp.open('GET', '/Act', true);
xmlhttp.setRequestHeader("p", "p");
xmlhttp.send();
}
But in addition to request from this function I can address to action, having collected in an address line of the browser website.com/Act value. This inadmissible behavior of my site. How to prevent such action of the user correctly ?
You can check that in the controller action using Request.IsAjaxRequest.For your action it can be done as below:
public string Act()
{
if(Request.IsAjaxRequest)
{
//AJAX work or response
}
//Non-AJAX work
}
You can even write a custom Attribute for this as below:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
var result = filterContext.Result as ViewResultBase;
if (result != null && result.Model != null)
{
filterContext.Result = new JsonResult
{
Data = result.Model,
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
}
}
}
This can be applied on the controller method just like other ffilters as below:
[AjaxOnly]
public string Act()
{
}
You can do that by changing your request to 'POST' instead of 'GET'. Then the action should be decorated with [HttpPost] attribute like this:
[HttpPost]
public string Act()
{
}