Why are the attributes of this model initialized to null? - c#

This is a follow up to the questions Custom form data with multiple files to Web API controller and Filling Parameter with Attribute [FromForm] with Fiddler, which are both answered by citing A MediaTypeFormatter for WebApi for multipart/form-data including file uploads.
Essentially this is the same question Tocana has asked before in the last link.
I copied the code from the latter, registered the media type formatter and tried it with fiddler, but as it enters UploadController.Post(UploadRequestViewModel model) model is instanciated but its attributes are all equal to null.
I debugged it and found that FormMultipartEncodedMediaTypeFormatter.BindToModel(IDictionary<string, object> data, Type type, IFormatterLogger formatterLogger) is called with a correct data dictionary, but from then on I cannot make head from tails. The binder used to bind the model is CompositeModelBinder, but wether that is correct or not I cannot tell.
The fiddler request looks like this:
Header:
Content-Type: multipart/form-data; boundary=-------------------------acebdf13572468
User-Agent: Fiddler
Host: localhost:49473
Content-Length: 13192
Body
---------------------------acebdf13572468
Content-Disposition: form-data; name="SiteId"
Content-Type: text/plain
987654321
---------------------------acebdf13572468
Content-Disposition: form-data; name="StartDate"
Content-Type: text/plain
24.08.1979
---------------------------acebdf13572468
Content-Disposition: form-data; name="Zulu"; filename="ExifTest.tif"
Content-Type: image/tiff
<#INCLUDE *C:\...\ExifTest.tif*#>
---------------------------acebdf13572468--
What do I have to do to get my model correctly initialized, i.e. its values are not null?
EDIT:
All models and controllers are those of A MediaTypeFormatter for WebApi for multipart/form-data including file uploads, i.e.
Model:
public class UploadRequestViewModel
{
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
[Required]
public HttpPostedFileBase File { get; set; }
}
Controller:
[Route("UploadTest")]
[HttpPost]
public IHttpActionResult Post(UploadRequestViewModel model)
{
}
FormMultipartEncodedMediaTypeFormatter.BindToModel
private object BindToModel(IDictionary<string, object> data, Type type, IFormatterLogger formatterLogger)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (type == null) throw new ArgumentNullException(nameof(type));
using (var config = new HttpConfiguration())
{
// if there is a requiredMemberSelector set, use this one by replacing the validator provider
var validateRequiredMembers = RequiredMemberSelector != null && formatterLogger != null;
if (validateRequiredMembers)
{
config.Services.Replace(typeof(ModelValidatorProvider), new RequiredMemberModelValidatorProvider(RequiredMemberSelector));
}
// create a action context for model binding
var actionContext = new HttpActionContext
{
ControllerContext = new HttpControllerContext
{
Configuration = config,
ControllerDescriptor = new HttpControllerDescriptor
{
Configuration = config
}
}
};
// create model binder context
var valueProvider = new NameValuePairsValueProvider(data, CultureInfo.InvariantCulture);
var metadataProvider = actionContext.ControllerContext.Configuration.Services.GetModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(null, type);
var modelBindingContext = new ModelBindingContext
{
ModelName = string.Empty,
FallbackToEmptyPrefix = false,
ModelMetadata = metadata,
ModelState = actionContext.ModelState,
ValueProvider = valueProvider
};
// bind model
var modelBinderProvider = new CompositeModelBinderProvider(config.Services.GetModelBinderProviders());
var binder = modelBinderProvider.GetBinder(config, type);
var haveResult = binder.BindModel(actionContext, modelBindingContext);
// log validation errors
if (formatterLogger != null)
{
foreach (var modelStatePair in actionContext.ModelState)
{
foreach (var modelError in modelStatePair.Value.Errors)
{
if (modelError.Exception != null)
{
formatterLogger.LogError(modelStatePair.Key, modelError.Exception);
}
else
{
formatterLogger.LogError(modelStatePair.Key, modelError.ErrorMessage);
}
}
}
}
return haveResult ? modelBindingContext.Model : GetDefaultValueForType(type);
}
}

Related

ASP.Net Web API Http routing and non JSON responses

I want to mimic behaviour of existing web service. Here is a very simplified example showing what I want to achieve.
I use ASP.Net Web API routing: it's quite simple to configure routes with it.
Requirements, part 1: query:
GET whatever.../Person/1
shall return JSON:
Content-Type: application/json; charset=utf-8
{"id":1,"name":"Mike"}
That's piece of cake:
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
}
// In ApiController
[HttpGet]
[Route("Person/{id}")]
public Person GetPerson(int id)
{
return new Person
{
ID = id,
Name = "Mike"
};
}
Requirements, part 2: query:
GET whatever.../Person/1?callback=functionName
shall return javascript:
Content-Type: text/plain; charset=utf-8
functionName({"id":1,"name":"Mike"});
Any ideas how to achieve this (part 2)?
The ApiController would need to be modified to satisfy the desired behavior
Simple example based on provided code
//GET whatever.../Person/1
//GET whatever.../Person/1?callback=functionName
[HttpGet]
[Route("Person/{id:int}")]
public IHttpActionResult GetPerson(int id, string callback = null) {
var person = new Person {
ID = id,
Name = "Mike"
};
if (callback == null) {
return Ok(person); // {"id":1,"name":"Mike"}
}
var response = new HttpResponseMessage(HttpStatusCode.OK);
var json = JsonConvert.SerializeObject(person);
//functionName({"id":1,"name":"Mike"});
var javascript = string.Format("{0}({1});", callback, json);
response.Content = new StringContent(javascript, Encoding.UTF8, "text/plain");
return ResponseMessage(response);
}
Of course you would need to do proper validation on the call back as this currently open up the API for script injection.

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! .

POST request throwing null exception

I am writing to seek help, in regards creating a POST request from private method. I currently have two methods GET and POST, which gets input parameters from private method. When I call the POST method using Fiddler:
Fiddler - Testing POST Request:
In the composer tab of the Fiddler, I copy in the URL -- [http://localhost:45361/api/test]. In the Request header box:
User-Agent: Fiddler
Content-Type: application/json;
Host: localhost:45361
Content-Length: 16
Authorization: Basic ###########==
In the Request Body box:
{"name":"prets"}
From the following above input, I get a NullReferenceException on the following line of code:
if (!string.IsNullOrEmpty(query.name))
However, if I call the GET method from the same private method, I am able to get okay response of 200, but not for POST request
I have tried adding a verification for the query object, but the POST request still shows the output as 404 error, i.e. "something went wrong with the query".
public HttpContext Current { get; set; }
[HttpGet]
public HttpResponseMessage get([FromUri] Query query)
{
return method(Current, query);
}
[HttpPost]
public HttpResponseMessage post([FromBody] Query query)
{
return method(Current, query);
}
private HttpResponseMessage method(HttpContext request, Query query)
{
if (User.IsInRole("admin"))
{
IQueryable<data> Data = null;
//verifying the query
if (query != null)
{
if (!string.IsNullOrEmpty(query.name))
{
var ids = query.name.Split(',');
var dataMatchingTags = db.database_data.Where(c => ids.Any(id => c.Name.Contains(id)));
if (Data == null)
Data = dataMatchingTags;
else
Data = Data.Union(dataMatchingTags);
}
if (Data == null)
Data = db.data;
if (query.endDate != null)
{
Data = Data.Where(c => c.UploadDate <= query.endDate);
}
Data = Data.OrderByDescending(c => c.UploadDate);
var data = Data.ToList();
if (!data.Any())
{
var message = string.Format("No data");
return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
}
return Request.CreateResponse(HttpStatusCode.OK, data);
}
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "something wrong with query statement.");
}
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Access Denied");
}
here is my object model class - Query:
public class Query
{
public string name { get; set; }
public Nullable<DateTime> startDate { get; set; }
public Nullable<DateTime> endDate{ get; set; }
}
I am calling the POST request from my controller only, as I have not embedded POST request from the client-end. I am using Fiddler, to test out the POST calls.
I am slightly struggling, in what other approach I can take, to get this resolved.
Any help would be very much appreciated.
Agree with earlier commenter, the nullpointer can't come from the line you say...
One null-pointer waiting to happen though is:
if (!string.IsNullOrEmpty(query.name))
{
//do something
}
if (Data == null)
{
Data = db.data; //this will always be null if the query object was null
}
This is assuming that you create/populate the 'db' object in the commented out block.

ASP.NET Web API not returning XML

I have a controller and method that adds user to a DB.
I call it from Fiddler with a Request Header as follows -
Content-Type: application/xml
Accept: application/xml
Host: localhost:62236
Content-Length: 39
And a request body of -
<User>
<Firstname>John</Firstname>
<Lastname>Doe</Lastname>
</User>
This works as expected, the method is called, the user object processed in the method PostUser.
public class UserController : ApiController
{
public HttpResponseMessage PostUser(User user)
{
// Add user to DB
var response = new HttpResponseMessage(HttpStatusCode.Created);
var relativePath = "/api/user/" + user.UserID;
response.Headers.Location = new Uri(Request.RequestUri, relativePath);
return response;
}
}
I am performing my Model Validation in it's own class
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
// Return the validation errors in the response body.
var errors = new Dictionary<string, IEnumerable<string>>();
foreach (KeyValuePair<string, ModelState> keyValue in actionContext.ModelState)
{
errors[keyValue.Key] = keyValue.Value.Errors.Select(e => e.ErrorMessage);
}
actionContext.Response =
actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
}
}
}
BUT if I post the following
<User>
<Firstname></Firstname> **//MISSING FIRST NAME**
<Lastname>Doe</Lastname>
</User>
The Model is invalid, and a JSON response is returned even though I stated Accept: application/xml.
If I perform model validation within the UserController, I get a proper XML response, but when I perform it in ModelValidationFilterAttribute I get JSON.
Your problems with the following code:
var errors = new Dictionary<string, IEnumerable<string>>();
actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
So you try to create the response from the errors object where its type is Dictionary<string, IEnumerable<string>>();.
Web.API will try to automatically find the right MediaTypeFormatter for your response type. However the default XML serializer (DataContractSerializer) not able to handle the type Dictionary<string, IEnumerable<string>>(); so it will use the JSON serializer for your response.
Actually you should use the CreateErrorResponse and just create the response from the ModelState directly (it will create a HttpError object which is XML seriazable)
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
actionContext.Response =
actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest,
actionContext.ModelState);
}
}
}
I think the Web API will return JSON as its default type for responses outside of controller methods.
Have you tried disabling the JSON formatter, as suggested in this article?
http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
i.e.
void ConfigureApi(HttpConfiguration config)
{
// Remove the JSON formatter
config.Formatters.Remove(config.Formatters.JsonFormatter);
}

MVC4 WebApi adding ETag in Response Header

We have a REST Service created in Mvc4
I am trying to add ETag Header in the Response from my WebApi method. It is added in the Header collection without any error but when I check the response header in the Fiddler it is not there.
Here is the method that I used to write header in the response:
internal static HttpResponseMessage<T> GetResponse<T>(Tuple<T, Dictionary<string, string>> response)
{
HttpResponseMessage<T> httpResponse = new HttpResponseMessage<T>(response.Item1, HttpStatusCode.OK);
if (response.Item2 != null)
{
foreach (var responseHeader in response.Item2)
{
if (string.Compare(responseHeader.Key, "ETAG", StringComparison.OrdinalIgnoreCase) == 0)
{
httpResponse.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue("\"" + responseHeader.Value + "\"");
}
else
{
httpResponse.Headers.Add(responseHeader.Key, responseHeader.Value);
}
}
}
return httpResponse;
}
You can do it 2 ways, you can either set the ETag in an ActionFilter.OnActionExecuted method like this:
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) {
actionExecutedContext.ActionContext.Response.Headers.ETag = new EntityTagHeaderValue(...);
}
But there's no way to easily pass the desired value from your controller to the ActionFilter. The second way is to change your WebAPI Action. Instead of returning a model type, return an HttpResponseMessage:
[HttpGet]
public HttpResponseMessage MyActionMethod() {
var result = // response data
var response = Request.CreateResponse<MyType>(HttpStatusCode.OK, result);
response.Headers.Add("Last Modified", result.Modified.ToString("R"));
response.Headers.ETag = new System.Net.Http.Headers.EntityTagHeaderValue(CreateEtag(result));
return response;
}

Categories