passing multiple objects as parameters to mvc 6 action - c#

I have an MVC 5 project that is working fine, and I need to migrate that project to MVC 6 with.net core. After managing to tweak everything to work I got stuck with a problem: Many of my actions accept more than 1 object as a parameter. The model binder MVC 5 is using has no problem with that, but MVC 6 seems to place null in all the parameters of such actions, I guess it's part of the unification of MVC and WebAPI. My question is if there is anyway around it without adding another model library of request wrapper objects.
for example:
[HttpPost]
public ActionResult GetVersionData(OvlEnvironment environment, Pipeline pipeline)
{
BL.SetEnviromentVersion(pipeline, environment);
return PartialView("_Version", environment);
}
On the mvc 5 project ajax requests containing json data in the form
{ "environment" : {*Data...*},
"pipeline" : {*Data...*}
}
were accepted. In mvc 6 both objects in response to the same request appear to be null.
Thanks

You have to give the ASP.NET Core MVC Framework the hint, that data to bind to the model is to be found in the body of the post request. This is done via the [FromBody] attribute.
[FromBody]: Use the configured formatters to bind data from the
request body. The formatter is selected based on content type of the
request.
By design it is not possible to bind multiple parameters to one JSON source as it is described here. In order to avoid additional model classes you can use a generic JObject like this:
[HttpPost]
public ActionResult GetVersionData([FromBody] JObject data)
{
var environment = data["environment"].ToObject<OvlEnvironment>();
var pipline = data["pipeline"].ToObject<Pipeline>();
BL.SetEnviromentVersion(pipeline, environment);
return PartialView("_Version", environment);
}

I know this post is old, but I also ran into this problem.
I think the accepted answer is right, in ASP.NET MVC5 it was possible to seek the HttpRequest.InputStream, but this is not allowed anymore in ASP.NET Core 2. So the work-around could be to read the stream once and store the parsed JObject in the ModelState (https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.modelstatedictionary?view=aspnetcore-2.1). Simple tests show that this approach could work.
This is an example of such an approach:
public class JsonMultiParameterModelBinder : IModelBinder
{
private static IModelBinder DefaultModelBinder;
private const string JSONKEY = "json_body";
public JsonMultiParameterModelBinder(IModelBinder defaultModelBinder)
{
DefaultModelBinder = defaultModelBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var httpContext = bindingContext.HttpContext;
var stream = bindingContext.HttpContext.Request.Body;
if (httpContext.Request.ContentType.Contains("application/json"))
{
JObject jobject = null;
ModelStateEntry entry = null;
bindingContext.ModelState.TryGetValue(JSONKEY, out entry);
if (entry == null)
{
using (var readStream = new StreamReader(stream, Encoding.UTF8))
using (var jsonReader = new JsonTextReader(readStream))
{
jobject = JObject.Load(jsonReader);
}
bindingContext.ModelState.SetModelValue(JSONKEY, jobject, null);
}
else
{
jobject = entry.RawValue as JObject;
}
var jobj = jobject[bindingContext.FieldName];
if (jobj == null)
{
httpContext.Response.StatusCode = 500; // error has occured
throw new JsonReaderException(string.Format("The rootnode '{0}' is not found in the Json message.", bindingContext.ModelName));
}
object obj = null;
var JSONSerializer = new JsonSerializer();
try
{
obj = jobj.ToObject(bindingContext.ModelType, JSONSerializer);
bindingContext.Model = obj;
bindingContext.Result = ModelBindingResult.Success(obj);
return Task.CompletedTask;
}
catch (Exception ex)
{
httpContext.Response.StatusCode = 500; // error has occured
throw ex;
}
}
return DefaultModelBinder.BindModelAsync(bindingContext);
}
}
And the provider for this binder :
public class JsonMultiParameterModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType)
{
ComplexTypeModelBinderProvider p = new ComplexTypeModelBinderProvider();
return new JsonMultiParameterModelBinder(p.GetBinder(context));
}
return null;
}
}
Register in Startup.cs:
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new JsonMultiParameterModelBinderProvider());
}

Related

How to validate json request body as valid json in asp.net core

In asp.net core 2.1, when a controller action is set as:
[HttpPost]
public JsonResult GetAnswer(SampleModel question)
{
return Json(question.Answer);
}
where the SampleModel is defined as:
public class SampleModel
{
[Required]
public string Question { get; set; }
public string Answer { get; set; }
}
this is still considered as valid request:
{
"question": "some question",
"question": "some question 2",
"answer": "some answer"
}
In the controller I can see that second question is the value of model and model is valid.
Question is how to validate just body of request as valid JSON even before model binding?
According to Timothy Shields's answer, it's hard to say that would be an invalid json if we have duplicated property keys.
And when using ASP.NET Core 2.1, it won't throw at all.
As of 12.0.1, the Newtonsoft.Json has a DuplicatePropertyNameHandling settings. It will throw if we set DuplicatePropertyNameHandling.Error and pass a duplicated property. So the easiest way I can come up is to create a custom model binder. We could deserialize the JSON and change the ModelState if it throws .
Fristly, install the latest Newtonsoft.Json:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
</ItemGroup>
and then register a JsonLoadSettings option as a singleton service for later reuse :
services.AddSingleton<JsonLoadSettings>(sp =>{
return new JsonLoadSettings {
DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error,
};
});
Now we can create a custom model binder to deal with duplicated properties :
public class XJsonModelBinder: IModelBinder
{
private JsonLoadSettings _loadSettings;
public XJsonModelBinder(JsonLoadSettings loadSettings)
{
this._loadSettings = loadSettings;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
var modelName = bindingContext.BinderModelName?? "XJson";
var modelType = bindingContext.ModelType;
// create a JsonTextReader
var req = bindingContext.HttpContext.Request;
var raw= req.Body;
if(raw == null){
bindingContext.ModelState.AddModelError(modelName,"invalid request body stream");
return Task.CompletedTask;
}
JsonTextReader reader = new JsonTextReader(new StreamReader(raw));
// binding
try{
var json= (JObject) JToken.Load(reader,this._loadSettings);
var o = json.ToObject(modelType);
bindingContext.Result = ModelBindingResult.Success(o);
}catch(Exception e){
bindingContext.ModelState.AddModelError(modelName,e.ToString()); // you might want to custom the error info
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
To enable read the Request.Body multiple times, we could also create a dummy Filter:
public class EnableRewindResourceFilterAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.HttpContext.Request.EnableRewind();
}
public void OnResourceExecuted(ResourceExecutedContext context) { }
}
Lastly, decorate the action method with [ModelBinder(typeof(XJsonModelBinder))] and EnableRewindResourceFilter:
[HttpPost]
[EnableRewindResourceFilter]
public JsonResult GetAnswer([ModelBinder(typeof(XJsonModelBinder))]SampleModel question)
{
if(ModelState.IsValid){
return Json(question.Answer);
}
else{
// ... deal with invalid state
}
}
Demo :

Asp WebApi: Prettify ActionResult JSON output

I'm working on REST service based on ASP.NET Core Web API and want to add a parameter 'prettify' to my endpoint so that response json will be formatted with indentation and readable in web browser.
My question - how can I change JSON formatting per controller method in ASP.WEB API Core application?
Appreciate you help.
Thank to #Nkosi comment, I've found the solution. Below is a code of the action filter that looks for 'prettify' parameter and adds indentation to output JSON. If the parameter omitted, indentation is also added.
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var actionResult = context.Result as ObjectResult;
if (actionResult == null) return;
var paramObj = context.HttpContext.Request.Query["prettify"];
var isPrettify = string.IsNullOrEmpty(paramObj) || bool.Parse(paramObj);
if (!isPrettify) return;
var settings = new JsonSerializerSettings { Formatting = Formatting.Indented };
actionResult.Formatters.Add(new JsonOutputFormatter(settings, ArrayPool<char>.Shared));
}
}
Create an action filter that can be used on which ever action you want to add that functionality. I had achieved the same in Asp.Net Web API 2 (not core) using a DelegatingHandler that would inspect the request and update the json formatter's indent based on prettify=true query parameter in the URL
Here is how it was done with the delegating handler
/// <summary>
/// Custom handler to allow pretty print json results.
/// </summary>
public class PrettyPrintJsonHandler : DelegatingHandler {
const string prettyPrintConstant = "pretty";
MediaTypeHeaderValue contentType = MediaTypeHeaderValue.Parse("application/json;charset=utf-8");
private System.Web.Http.HttpConfiguration httpConfig;
/// <summary>
/// Initializes a new instance of the <seealso cref="PrettyPrintJsonHandler"/> class with an HTTP Configuration.
/// </summary>
/// <param name="config"></param>
public PrettyPrintJsonHandler(System.Web.Http.HttpConfiguration config) {
this.httpConfig = config;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) {
var canPrettyPrint = checkQuery(request.RequestUri.Query);
var jsonFormatter = httpConfig.Formatters.JsonFormatter;
jsonFormatter.Indent = canPrettyPrint;
var response = await base.SendAsync(request, cancellationToken);
if (canPrettyPrint && response.Content != null) {
response.Content.Headers.ContentType = contentType;
}
return response;
}
private bool checkQuery(string queryString) {
var canPrettyPrint = false;
if (!string.IsNullOrWhiteSpace(queryString)) {
var prettyPrint = QueryString.Parse(queryString)[prettyPrintConstant];
canPrettyPrint = !string.IsNullOrWhiteSpace(prettyPrint) && Boolean.TryParse(prettyPrint, out canPrettyPrint) && canPrettyPrint;
}
return canPrettyPrint;
}
}
which was added as a global message handler during setup.
The same concept could be applied to an action filter.
Based on #Valentine's answer, and for Newtonsoft Json.NET (and .NET Core), I came up with this action filter implementation, which formats the response for content values except where the content value type is string:
public class OutputFormatActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
}
public void OnActionExecuted(ActionExecutedContext context)
{
var objectResult = context.Result as ObjectResult;
if (objectResult?.Value == null) return;
if (objectResult.Value is string) return; // ignore string values - it would add extra quotes around the string in the response
var jsonFormatter = new NewtonsoftJsonOutputFormatter(
new JsonSerializerSettings
{
// set custom json settings here
},
ArrayPool<char>.Shared,
new MvcOptions(),
new MvcNewtonsoftJsonOptions()
);
objectResult.Formatters.Add(jsonFormatter);
}
}
Register the action filter in the Startup.ConfigureServices to apply it to all MVC/Web API controllers:
services.AddControllersWithViews(options =>
{
options.Filters.Add(new OutputFormatActionFilter());
});

Upload files and JSON in ASP.NET Core Web API

How can I upload a list of files (images) and json data to ASP.NET Core Web API controller using multipart upload?
I can successfully receive a list of files, uploaded with multipart/form-data content type like that:
public async Task<IActionResult> Upload(IList<IFormFile> files)
And of course I can successfully receive HTTP request body formatted to my object using default JSON formatter like that:
public void Post([FromBody]SomeObject value)
But how can I combine these two in a single controller action? How can I upload both images and JSON data and have them bind to my objects?
Simple, less code, no wrapper model
There is simpler solution, heavily inspired by Andrius' answer. By using
the ModelBinderAttribute you don't have to specify a model or binder provider. This saves a lot of code. Your controller action would look like this:
public IActionResult Upload(
[ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
IList<IFormFile> files)
{
// Use serialized json object 'value'
// Use uploaded 'files'
}
Implementation
Code behind JsonModelBinder (see GitHub or use NuGet package):
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
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;
}
}
Example request
Here is an example of a raw http request as accepted by the controller action Upload above.
A multipart/form-data request is split into multiple parts each separated by the specified boundary=12345. Each part got a name assigned in its Content-Disposition-header. With these names default ASP.Net-Core knows which part is bound to which parameter in the controller action.
Files that are bound to IFormFile additionally need to specify a filename as in the second part of the request. Content-Type is not required.
Another thing to note is that the json parts need to be deserializable into the parameter types as defined in the controller action. So in this case the type SomeObject should have a property key of type string.
POST http://localhost:5000/home/upload HTTP/1.1
Host: localhost:5000
Content-Type: multipart/form-data; boundary=12345
Content-Length: 218
--12345
Content-Disposition: form-data; name="value"
{"key": "value"}
--12345
Content-Disposition: form-data; name="files"; filename="file.txt"
Content-Type: text/plain
This is a simple text file
--12345--
Testing with Postman
Postman can be used to call the action and test your server side code. This is quite simple and mostly UI driven. Create a new request and select form-data in the Body-Tab. Now you can choose between text and file for each part of the reqeust.
I'm working with Angular 7 on the front-end, so I make use of the FormData class, which allows you to append strings or blobs to a form. They can be pulled out of the form in the controller action using the [FromForm] attribute. I add the file to the FormData object, and then I stringify the data I wish to send together with the file, append it to the FormData object, and deserialize the string in my controller action.
Like so:
//front-end:
let formData: FormData = new FormData();
formData.append('File', fileToUpload);
formData.append('jsonString', JSON.stringify(myObject));
//request using a var of type HttpClient
http.post(url, formData);
//controller action
public Upload([FromForm] IFormFile File, [FromForm] string jsonString)
{
SomeType myObj = JsonConvert.DeserializeObject<SomeType>(jsonString);
//do stuff with 'File'
//do stuff with 'myObj'
}
You now have a handle on the file and the object. Note that the name you provide in the params list of your controller action must match the name you provide when appending to the FormData object on the front-end.
Apparently there is no built in way to do what I want. So I ended up writing my own ModelBinder to handle this situation. I didn't find any official documentation on custom model binding but I used this post as a reference.
Custom ModelBinder will search for properties decorated with FromJson attribute and deserialize string that came from multipart request to JSON. I wrap my model inside another class (wrapper) that has model and IFormFile properties.
IJsonAttribute.cs:
public interface IJsonAttribute
{
object TryConvert(string modelValue, Type targertType, out bool success);
}
FromJsonAttribute.cs:
using Newtonsoft.Json;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FromJsonAttribute : Attribute, IJsonAttribute
{
public object TryConvert(string modelValue, Type targetType, out bool success)
{
var value = JsonConvert.DeserializeObject(modelValue, targetType);
success = value != null;
return value;
}
}
JsonModelBinderProvider.cs:
public class JsonModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType)
{
var propName = context.Metadata.PropertyName;
var propInfo = context.Metadata.ContainerType?.GetProperty(propName);
if(propName == null || propInfo == null)
return null;
// Look for FromJson attributes
var attribute = propInfo.GetCustomAttributes(typeof(FromJsonAttribute), false).FirstOrDefault();
if (attribute != null)
return new JsonModelBinder(context.Metadata.ModelType, attribute as IJsonAttribute);
}
return null;
}
}
JsonModelBinder.cs:
public class JsonModelBinder : IModelBinder
{
private IJsonAttribute _attribute;
private Type _targetType;
public JsonModelBinder(Type type, IJsonAttribute attribute)
{
if (type == null) throw new ArgumentNullException(nameof(type));
_attribute = attribute as IJsonAttribute;
_targetType = type;
}
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;
bool success;
var result = _attribute.TryConvert(valueAsString, _targetType, out success);
if (success)
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Usage:
public class MyModelWrapper
{
public IList<IFormFile> Files { get; set; }
[FromJson]
public MyModel Model { get; set; } // <-- JSON will be deserialized to this object
}
// Controller action:
public async Task<IActionResult> Upload(MyModelWrapper modelWrapper)
{
}
// Add custom binder provider in Startup.cs ConfigureServices
services.AddMvc(properties =>
{
properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
});
Following the excellent answer by #bruno-zell, if you have only one file (I didn't test with an IList<IFormFile>) you can also just declare your controller as this :
public async Task<IActionResult> Create([FromForm] CreateParameters parameters, IFormFile file)
{
const string filePath = "./Files/";
if (file.Length > 0)
{
using (var stream = new FileStream($"{filePath}{file.FileName}", FileMode.Create))
{
await file.CopyToAsync(stream);
}
}
// Save CreateParameters properties to database
var myThing = _mapper.Map<Models.Thing>(parameters);
myThing.FileName = file.FileName;
_efContext.Things.Add(myThing);
_efContext.SaveChanges();
return Ok(_mapper.Map<SomeObjectReturnDto>(myThing));
}
Then you can use the Postman method shown in Bruno's answer to call your controller.
I had a similar issue and I solved the problem by using [FromForm] attribute and FileUploadModelView in the function as follows:
[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
{
return null;
}
I wanted to do the same using Vue frontend and .net core api. But for some weird reason IFormFile always returned null. So then I had to change it to IFormCollection and got it sorted out. Here is the code for anyone facing the same issue :)
public async Task<IActionResult> Post([FromForm]IFormCollection files)
An updated version for .net 5 based on #bruno-zell 's answer with added support to multiple files
using System;
using System.Collections;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
public class JsonModelBinder : IModelBinder
{
private readonly JsonOptions _jsonOptions;
public JsonModelBinder(IOptions<JsonOptions> jsonOptions)
{
_jsonOptions = jsonOptions.Value;
}
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);
string toSerialize;
// Attempt to convert the input value
if (typeof(IEnumerable).IsAssignableFrom(bindingContext.ModelType))
{
toSerialize = "[" + string.Join<string>(',', valueProviderResult.Values) + "]";
}
else
{
toSerialize = valueProviderResult.FirstValue;
}
var result = JsonSerializer.Deserialize(toSerialize, bindingContext.ModelType, _jsonOptions.JsonSerializerOptions);
if (result != null)
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
You don't need "JsonModelBinder" and other custom stuff, I have model
public class UpdateAdminProfileInfoRequest
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Mobile { get; set; }
public IFormFile Image { get; set; }
}
and in Controller action method only one argument
[FromForm]UpdateAdminProfileInfoRequest request
and everything works fine.
If you need multiple file upload just change
IFormFile
with
List<IFormFile> files {get; set}
Be careful I don't know why but currently I use .net6 and nor JsonProperty nor JsonPropertyName don't work on IFormFile, if you decorate Image property with
[JsonProperty("imageFile")]
attribute or something like this asp.net don't map client "imageFile" field to "Image" property.
I am not sure if you can do the two things in a single step.
How I have achieved this in the past is by uploading the file through ajax and returning the file url back in the response and then pass it along with post request to save the actual record.
I had a similar problem when posting from angular to asp core api.
Chrome:
Form Data
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file1"
undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file2"
undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="reportData"; filename="blob"
Content-Type: application/json
{"id":2,"report":3,"code":"XX0013","business":"01","name":"Test","description":"Description"}
------WebKitFormBoundarydowgB6BX0wiwKeOk--
Here is how I do it:
I use reportData as an uploaded file data, then I read the file's contents.
[HttpPost]
public async Task<IActionResult> Set([FromForm] IFormFile file1, [FromForm] IFormFile file2, [FromForm] IFormFile reportData)
{
try
{
ReportFormModel.Result result = default;
if (reportData != null)
{
string reportJson = await reportData.ReadFormFileAsync();
ReportFormModel.Params reportParams = reportJson.JsonToObject<ReportFormModel.Params>();
if (reportParams != null)
{
//OK
}
}
return Ok(result);
}
catch (Exception ex)
{
return BadRequest();
}
}
public static class Utilities
{
public static async Task<string> ReadFormFileAsync(this IFormFile file)
{
if (file == null || file.Length == 0)
{
return await Task.FromResult((string)null);
}
using var reader = new StreamReader(file.OpenReadStream());
return await reader.ReadToEndAsync();
}
}
Although this way is not appreciated, but it worked.
I'm just adding my two cents.
The Angular app function for uploading file with json
uploadFileWithJson = (files) => {
if (files.length === 0) {
return;
}
let fileToUpload = <File>files[0];
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);
const user = {
"name":"Vikram",
"age":35
}
const userTxt = JSON.stringify(user);
formData.append('userData',userTxt);
this.http.post('https://localhost:5001/api/upload/UploadFileWithJson', formData, {reportProgress: true, observe: 'events'})
.subscribe({
next: (event) => {
if (event.type === HttpEventType.UploadProgress)
this.progress = Math.round(100 * event.loaded / event.total);
else if (event.type === HttpEventType.Response) {
this.message = 'Upload success.';
this.onUploadFinished.emit(event.body);
}
},
error: (err: HttpErrorResponse) => console.log(err)
});
}
And .NET Core API
[HttpPost("UploadFileWithJson"), DisableRequestSizeLimit]
public IActionResult UploadFileWithJson([FromForm]UserWithFile model)
{
try
{
if (model == null)
throw new Exception($"{nameof(model)} is null");
if (model.File == null)
throw new Exception("File is null");
var folderName = Path.Combine("Resources", "Images");
var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName);
if (model.File.Length > 0)
{
var fileName = ContentDispositionHeaderValue.Parse(model.File.ContentDisposition).FileName.Trim('"');
var fullPath = Path.Combine(pathToSave, fileName);
var dbPath = Path.Combine(folderName, fileName);
using (var stream = new FileStream(fullPath, FileMode.Create))
{
model.File.CopyTo(stream);
}
return Ok(new { dbPath });
}
else
{
return BadRequest();
}
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex}");
}
}
as well as model class.
public class UserWithFile
{
public string UserData { get; set; }
public IFormFile File { get; set; }
}
Now notice, in Angular app, the FormData attribute file name first letter f is small-cased. However, in .NET Core same is File (or upper-cased). Same is case for userData.
I simply changed file and userData in Angular app to File and UserData respectively.
Voila! .

Using JSON.NET to return ActionResult [duplicate]

This question already has answers here:
Json.Net And ActionResult
(2 answers)
Closed 5 years ago.
I'm trying to write a C# method that will serialize a model and return a JSON result. Here's my code:
public ActionResult Read([DataSourceRequest] DataSourceRequest request)
{
var items = db.Words.Take(1).ToList();
JsonSerializerSettings jsSettings = new JsonSerializerSettings();
jsSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
var converted = JsonConvert.SerializeObject(items, null, jsSettings);
return Json(converted, JsonRequestBehavior.AllowGet);
}
I got the following JSON result when I go to Words/Read in Chrome:
"[{\"WordId\":1,\"Rank\":1,\"PartOfSpeech\":\"article\",\"Image\":\"Upload/29/1/Capture1.PNG\",\"FrequencyNumber\":\"22038615\",\"Article\":null,\"ClarificationText\":null,\"WordName\":\"the | article\",\"MasterId\":0,\"SoundFileUrl\":\"/UploadSound/7fd752a6-97ef-4a99-b324-a160295b8ac4/1/sixty_vocab_click_button.mp3\",\"LangId\":1,\"CatId\":null,\"IsActive\":false}
I think the \" escaped quotes are a problem that occurs when you double serialize an object. From other questions:
WCF JSON output is getting unwanted quotes & backslashes added
It definitely looks like I'm double serializing my object, since I first serialize using JSON.NET and then pass my result into the Json() function. I need to manually serialize to avoid referenceloops, but I think my View needs an ActionResult.
How can I return an ActionResult here? Do I need to, or can I just return a string?
I found a similar stackoverflow question:
Json.Net And ActionResult
The answer there suggested using
return Content( converted, "application/json" );
That seems to work on my very simple page.
Instead of serializing using JSON.NET and then calling Json(), why not instead override the Json() method in your controller (or perhaps a base controller to enhance its re-usability)?
This is pulled from this blog post.
In your controller (or base controller):
protected override JsonResult Json(
object data,
string contentType,
System.Text.Encoding contentEncoding,
JsonRequestBehavior behavior)
{
return new JsonNetResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding,
JsonRequestBehavior = behavior
};
}
And the definition for JsonNetResult:
public class JsonNetResult : JsonResult
{
public JsonNetResult()
{
Settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
};
}
public JsonSerializerSettings Settings { get; private set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet
&& "GET".Equals(
context.HttpContext.Request.HttpMethod,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("JSON GET is not allowed");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType =
string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;
if (this.ContentEncoding != null)
response.ContentEncoding = this.ContentEncoding;
if (this.Data == null)
return;
var scriptSerializer = JsonSerializer.Create(this.Settings);
using (var sw = new StringWriter())
{
scriptSerializer.Serialize(sw, this.Data);
response.Write(sw.ToString());
}
}
}
By doing this, when you call Json() in your controller, you will automatically get the JSON.NET serializing you want.

Web Api Required Parameter

Using ASP.NET Web API. Is there a way to automatically return a status code 400 if a parameter is null? I found this question but that is a global solution that is applied to all methods, I want to do this on a per method per parameter basis.
So, for example, this is what I am currently doing:
public HttpResponseMessage SomeMethod(SomeNullableParameter parameter)
{
if (parameter == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
// Otherwise do more stuff.
}
I would really just like to do something like this (notice the required attribute):
public HttpResponseMessage SomeMethod([Required] SomeNullableParameter parameter)
{
// Do stuff.
}
The approach I ended up using was to create a custom filter that I registered globally. The filter checks all request parameters for the RequiredAttribute. If the attribute is found then it checks if the parameter was passed with the request (not null) and returns status code 400 if it was null. I also added a cache to the filter to store the required parameters for each request to avoid the reflection hit on future calls. I was pleasantly surprised to find that this works for value types as well since the action context stores the parameters as objects.
EDIT - Updated solution based on tecfield's comment
public class RequiredParametersFilter : ActionFilterAttribute
{
// Cache used to store the required parameters for each request based on the
// request's http method and local path.
private readonly ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>> _Cache =
new ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Get the request's required parameters.
List<string> requiredParameters = this.GetRequiredParameters(actionContext);
// If the required parameters are valid then continue with the request.
// Otherwise, return status code 400.
if(this.ValidateParameters(actionContext, requiredParameters))
{
base.OnActionExecuting(actionContext);
}
else
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
private bool ValidateParameters(HttpActionContext actionContext, List<string> requiredParameters)
{
// If the list of required parameters is null or containst no parameters
// then there is nothing to validate.
// Return true.
if (requiredParameters == null || requiredParameters.Count == 0)
{
return true;
}
// Attempt to find at least one required parameter that is null.
bool hasNullParameter =
actionContext
.ActionArguments
.Any(a => requiredParameters.Contains(a.Key) && a.Value == null);
// If a null required paramter was found then return false.
// Otherwise, return true.
return !hasNullParameter;
}
private List<string> GetRequiredParameters(HttpActionContext actionContext)
{
// Instantiate a list of strings to store the required parameters.
List<string> result = null;
// Instantiate a tuple using the request's http method and the local path.
// This will be used to add/lookup the required parameters in the cache.
Tuple<HttpMethod, string> request =
new Tuple<HttpMethod, string>(
actionContext.Request.Method,
actionContext.Request.RequestUri.LocalPath);
// Attempt to find the required parameters in the cache.
if (!this._Cache.TryGetValue(request, out result))
{
// If the required parameters were not found in the cache then get all
// parameters decorated with the 'RequiredAttribute' from the action context.
result =
actionContext
.ActionDescriptor
.GetParameters()
.Where(p => p.GetCustomAttributes<RequiredAttribute>().Any())
.Select(p => p.ParameterName)
.ToList();
// Add the required parameters to the cache.
this._Cache.TryAdd(request, result);
}
// Return the required parameters.
return result;
}
}
Set [Required] on a property in your model and then check the ModelState to see if it IsValid.
This will allow all the required properties to be tested at the same time.
See the "Under-Posting" section # Model validation in WebAPI
we can use the BindRequired, which is from Microsoft.AspNetCore.Mvc.ModelBinding namespace.
public async Task<ActionResult<IEnumerable<Numbers>>> GetAll([BindRequired, FromQuery]string[] numbers)
{
var result = await _service.GetAllDetails(numbers);
return Ok(result);
}
after that your swagger will look like below.
A solution for asp.net core...
[AttributeUsage(AttributeTargets.Method)]
public sealed class CheckRequiredModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var requiredParameters = context.ActionDescriptor.Parameters.Where(
p => ((ControllerParameterDescriptor)p).ParameterInfo.GetCustomAttribute<RequiredModelAttribute>() != null).Select(p => p.Name);
foreach (var argument in context.ActionArguments.Where(a => requiredParameters.Contains(a.Key, StringComparer.Ordinal)))
{
if (argument.Value == null)
{
context.ModelState.AddModelError(argument.Key, $"The argument '{argument.Key}' cannot be null.");
}
}
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
context.Result = new BadRequestObjectResult(errors);
return;
}
base.OnActionExecuting(context);
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class RequiredModelAttribute : Attribute
{
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(CheckRequiredModelAttribute));
});
public async Task<IActionResult> CreateAsync([FromBody][RequiredModel]RequestModel request, CancellationToken cancellationToken)
{
//...
}
The accepted solution takes it upon itself to report back any errors. A more appropriate approach for MVC5 is to let the controller handle (via model validation) the reporting of any errors, aka something like this:
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class ValidateParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
var descriptor = context.ActionDescriptor;
if (descriptor != null)
{
var modelState = context.ModelState;
foreach (var parameterDescriptor in descriptor.GetParameters())
{
EvaluateValidationAttributes(
suppliedValue: context.ActionArguments[parameterDescriptor.ParameterName],
modelState: modelState,
parameterDescriptor: parameterDescriptor
);
}
}
base.OnActionExecuting(context);
}
static private void EvaluateValidationAttributes(HttpParameterDescriptor parameterDescriptor, object suppliedValue, ModelStateDictionary modelState)
{
var parameterName = parameterDescriptor.ParameterName;
parameterDescriptor
.GetCustomAttributes<object>()
.OfType<ValidationAttribute>()
.Where(x => !x.IsValid(suppliedValue))
.ForEach(x => modelState.AddModelError(parameterName, x.FormatErrorMessage(parameterName)));
}
}
You may then plug it in universally via WebApiConfig.cs:
config.Filters.Add(new ValidateParametersAttribute());

Categories