MVC controller encoding JSON with Json(object) vs JavaScriptSerializer().Serialize(object) - c#

In my current MVC application I perform an AJAX call to the controller. The controller gets an object from the database and returns it in the form of a JSON response. My confusion is between using the Controller.Json method vs JavaScriptSerializer.Serialize(object). My object I am converting is a simple class called PhoneSerializable with a few public properties of type string and int.
I have the following:
var serializedPhone = jsSerializer.Serialize(new PhoneSerializable(phone));
return new ContentResult {Content = serializedPhone, ContentType = "application/json"};
This works correctly and I get a JSON response that my AJAX caller can parse.
Yet some online examples I have seen use this instead:
return Json(new PhoneSerializable(phone));
The response of this has mostly null properties and a DenyGet header which means it failed parsing or something. The documentation for the Json(object) method states that it uses the same javascript serializer for the object yet it fails to convert my simple object to JSON.
What am I missing? I would like to use Json(object) since it is much cleaner but I cannot seem to get it to work.
EDIT WITH ANSWER:
The Json() method was defaulting to "DenyGet" adding more parameters fixed the issue and it now works:
return Json(new PhoneSerializable(phone),
"application/json",
JsonRequestBehavior.AllowGet);

Using the Json method from the Controller base class simply returns a new object of type JsonResult, using the following overload:
protected internal JsonResult Json(object data)
{
return this.Json(data, null, null, JsonRequestBehavior.DenyGet);
}
protected internal virtual JsonResult Json(
object data,
string contentType,
Encoding contentEncoding,
JsonRequestBehavior behavior)
{
return new JsonResult
{
Data = data,
ContentType = contentType,
ContentEncoding = contentEncoding,
JsonRequestBehavior = behavior
};
}
After you return Json(new PhoneSerialize(phone));, the controller eventually invokes a method called ExecuteResult(ControllerContext context), which internally does the same serialization with JsonSerializer:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
string.Equals(context.HttpContext.Request.HttpMethod,
"GET",
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);
}
HttpResponseBase response = context.HttpContext.Response;
if (!string.IsNullOrEmpty(this.ContentType))
{
response.ContentType = this.ContentType;
}
else
{
response.ContentType = "application/json";
}
if (this.ContentEncoding != null)
{
response.ContentEncoding = this.ContentEncoding;
}
if (this.Data != null)
{
JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
if (this.MaxJsonLength.HasValue)
{
javaScriptSerializer.MaxJsonLength = this.MaxJsonLength.Value;
}
if (this.RecursionLimit.HasValue)
{
javaScriptSerializer.RecursionLimit = this.RecursionLimit.Value;
}
response.Write(javaScriptSerializer.Serialize(this.Data));
}
}
This basically means you're passing both DenyGet as a default parameter, yet your method is a "GET", which ends up throwing an InvalidOperationException. You should specify the "AllowGet" explicitly to avoid this:
return Json(new PhoneSerializable(phone),
"application/json",
JsonRequestBehavior.AllowGet);

Related

How to get around HttpResponseMessage (Request.CreateResponse) in .net Core

protected HttpResponseMessage CreatedResponse(string routeName, object routeValues)
{
var response = Request.CreateResponse(HttpStatusCode.Created);
var locationUri = Url.Link(routeName, routeValues);
response.Headers.Location = new Uri(locationUri);
return response;
}
Whats the equivalent code in .net Core?
Or a way around it..
Thanks
you need to use Controller.Created method, but now it also requires information about URI:
public IActionResult CreatedResponse(object value)
{
return this.Created(<string uri>, value);
//or
return this.Created(<Uri uri>, value);
}
Actually, in background method creates and returns the CreatedResult object, that is derived from ObjectResult, and fill Location, Value and StatusCode fields. So, as alternative, you may create general ObjectResult response if you don't need to return the URI.
public IActionResult CreatedResponse(object value)
{
return new ObjectResult
{
Value = value,
StatusCode = Microsoft.AspNetCore.Http.StatusCodes.Status201Created // 201
};
}
I built upon the other answer, and created an extension method. You will have to return an IActionResult. So if you still want to do it the old fashion way, you can with this:
public static class HttpRequestExtensions
{
public static IActionResult CreateResponse(this HttpRequest request, int status, object content)
{
return new ObjectResult(content)
{
StatusCode = status
};
}
}
And in use:
public IActionResult Patch(long id, [FromBody]JsonPatchDocument<SomeModel> value)
{
//other code here for patching logic
var response = new { Original = modelBefore, Patched = modelAfter };
return Request.CreateResponse(StatusCodes.Status200OK, response);
}

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.

When to use HttpResponseMessage and Request.CreateResponse

When should we use the HttpResponseMessage object and when should we use the Request.CreateResponse(...) method?
Also, what is the difference between the HttpResponseMessage object and the Request.CreateResponse(...) method?
What is difference between HttpResponseMessage object and
Request.CreateResponse(...) method?
It is probably obvious but Request.CreateResponse is a helper method for creating HttpResponseMessage object.
When we must use HttpResponseMessage object and When we must use
Request.CreateResponse(...) method?
If you want to use the built-in content negotiation feature, use Request.CreateResponse. When you return an object, ASP.NET Web API has to serialize the object into response body. This could be generally JSON or XML (other media types are possible but you need to create the formatter). The media type chosen (JSON or XML) is based on the request content type, Accept header in the request and so on and content negotiation is the process that determines the media type to be used. By using Request.CreateResponse, you are automatically using the result of this process.
On the other hand, if you create HttpResponseMessage yourself, you have to specify a media formatter based on which the object will be serialized and by specifying the media formatter yourself, you can override the results of conneg.
EDIT
Here is an example of how to specify JSON formatter.
public HttpResponseMessage Get(int id)
{
var foo = new Foo() { Id = id };
return new HttpResponseMessage()
{
Content = new ObjectContent<Foo>(foo,
Configuration.Formatters.JsonFormatter)
};
}
With this, even if you send a request with Accept:application/xml, you will only get JSON.
Request.CreateResponse(...) is just a builder, it also returns instance of HttpResponseMessage, here is the code:
public static HttpResponseMessage CreateResponse<T>(this HttpRequestMessage request, HttpStatusCode statusCode, T value, HttpConfiguration configuration)
{
if (request == null)
throw Error.ArgumentNull("request");
configuration = configuration ?? HttpRequestMessageExtensions.GetConfiguration(request);
if (configuration == null)
throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoConfiguration, new object[0]);
IContentNegotiator contentNegotiator = ServicesExtensions.GetContentNegotiator(configuration.Services);
if (contentNegotiator == null)
{
throw Error.InvalidOperation(SRResources.HttpRequestMessageExtensions_NoContentNegotiator, new object[1]
{
(object) typeof (IContentNegotiator).FullName
});
}
else
{
IEnumerable<MediaTypeFormatter> formatters = (IEnumerable<MediaTypeFormatter>) configuration.Formatters;
ContentNegotiationResult negotiationResult = contentNegotiator.Negotiate(typeof (T), request, formatters);
if (negotiationResult == null)
{
return new HttpResponseMessage()
{
StatusCode = HttpStatusCode.NotAcceptable,
RequestMessage = request
};
}
else
{
MediaTypeHeaderValue mediaType = negotiationResult.MediaType;
return new HttpResponseMessage()
{
Content = (HttpContent) new ObjectContent<T>(value, negotiationResult.Formatter, mediaType),
StatusCode = statusCode,
RequestMessage = request
};
}
}

Change Response Headers on Media Type Formatter for ASP.NET Web API

I have created an ASP.NET web API controller that is returning a strongly typed object on an action, as follows:
// GET api/iosdevices/5
public iOSDevice Get(string id) {
return new iOSDevice();
}
I have created a BufferedMediaTypeFormatter to handle the type iOSDevice:
public class iOSDeviceXmlFormatter : BufferedMediaTypeFormatter
{
public iOSDeviceXmlFormatter() {
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content) {
content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
iOSDevice device = (iOSDevice)value;
using (XmlWriter writer = XmlWriter.Create(writeStream)) {
writer.WriteStartElement("iOSDevice");
if (device.identifierForVendor != Guid.Empty) {
writer.WriteElementString("identifierForVendor", device.identifierForVendor.ToString());
writer.WriteElementString("userInterfaceIdiom", device.userInterfaceIdiom);
writer.WriteElementString("systemName", device.systemName);
writer.WriteElementString("systemVersion", device.systemVersion);
writer.WriteElementString("model", device.model);
}
writer.WriteEndElement();
}
writeStream.Close();
}
}
My problem is when I catch type "text/html" (e.g. someone attempts to access the API from his or her web browser), the response type is "text/html" instead of "application/xml". I want to override the response type so that the user gets a response that is "application/xml" instead of "text/html".
I cannot in the ApiController type get access to the "Response" property that is on regular MVC controllers and I am at a loss. How do I override the response type for this action that is being handled by a media type formatter?
EDIT: HELPFUL NOTE
I was trying this previously:
var response = Request.CreateResponse<iOSDevice>(HttpStatusCode.Accepted, device);
response.Headers.Remove("Content-Type");
response.Headers.Add("Content-Type", "application/xml; charset=utf-8");
return response;
And it claimed I was "misusing" the headers.
But when I used Filip's example below of setting Content directly, it worked!
var response = Request.CreateResponse();
response.Content = new ObjectContent<iOSDevice>(device, new iOSDeviceXmlFormatter());
return response;
When you write to stream in the formatter, headers have been already sent.
You can do this:
public HttpResponseMessage Get(string id) {
{
var value = new iOSDevice();
var response = Request.CreateResponse();
response.Content = new ObjectContent(typeof(iOSDevice), value, new iOSDeviceXmlFormatter());
//set headers on the "response"
return response;
}
or you can do this (add this method to your formatter):
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, string mediaType)
{
base.SetDefaultContentHeaders(type, headers, mediaType);
headers.ContentType = new MediaTypeHeaderValue("application/xml");
}
Here is an example on how I used the SetDefaultContentHeaders with a custom formatter:
http://www.strathweb.com/2012/09/generate-kindle-mobi-ebooks-with-your-asp-net-web-api/
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)
{
if (CanWriteType(type) && mediaType.MediaType == supportedMediaType)
{
headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
headers.ContentDisposition.FileName = "ebook.mobi";
}
else
{
base.SetDefaultContentHeaders(type, headers, mediaType);
}
}

Categories