Minimal API and XML formatters - asp.net

Trying out minimal APIs in .NET 6 and can't make it work with XML content type. If I use standard controllers, using .AddXmlSerializerFormatters() extension does the job:
builder.Services.AddControllers().AddXmlSerializerFormatters();
But when I switch from controller to .MapPost(..), I start getting 415 HTTP responses.
app.MapPost("/endpoint", ([FromBody] Request request) => {})
.Accepts<Request>("text/xml");
HTTP response: 415 Microsoft.AspNetCore.Http.BadHttpRequestException: Expected a
supported JSON media type but got "text/xml"
Is there any other way I can declare XML formatters that will work with minimal APIs?

As suggested by the post linked by guru-stron, it's possible to pass XML documents by implementing your own wrapping model that provides a BindAsync method.
internal sealed class XDocumentModel
{
public XDocumentModel(XDocument document) => Document = document;
public XDocument Document { get; init; }
public static async ValueTask<XDocumentModel?> BindAsync(HttpContext context, ParameterInfo parameter)
{
if (!context.Request.HasXmlContentType())
throw new BadHttpRequestException(
message: "Request content type was not a recognized Xml content type.",
StatusCodes.Status415UnsupportedMediaType);
return new XDocumentModel(await XDocument.LoadAsync(context.Request.Body, LoadOptions.None, CancellationToken.None));
}
}
I added a extension method to HttpRequest for convenient Content-Type validation.
internal static class HttpRequestXmlExtensions
{
public static bool HasXmlContentType(this HttpRequest request)
=> request.Headers.TryGetValue("Content-Type", out var contentType)
&& string.Equals(contentType, "application/xml", StringComparison.InvariantCulture);
}
You can then use the model directly as a paramter by your minimal API endpoint.
app.MapGet("/xml-test", (XDocumentModel model) =>
{
// model.Document <- your passed xml Document
return Results.Ok(new { Value = model.Document.ToString() });
})
Some final thoughts: This implementation enables you to pass a generic XML document to the endpoint. However, if you expect a certain document structure, you could implement this by making the XDocumentModel expect a generic type parameter and extracting this type's properties from the XDocument instance.

I did it this way:
app.MapPost("/endpoint", (HttpContext c) =>
{
var reader = new StreamReader(c.Request.Body);
var xml = reader.ReadToEndAsync().Result;
// You can do with your xml string whatever you want
return Results.Ok();
}).Accepts<HttpRequest>("application/xml");

Related

How to use built-in xml or json formatter for custom accept header value in .Net Core 2.0

Update:
I have uploaded a small test project to github: link
I am creating a small web service with .Net Core 2, and would like to give the ability to clients to specify if they need navigational info in the response or not. The web api should only support xml and json, but it would be nice if clients could use
Accept: application/xml+hateoas
or
Accept: application/json+hateoas
in their request.
I tried setting up my AddMvc method like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
options.FormatterMappings.SetMediaTypeMappingForFormat(
"xml", MediaTypeHeaderValue.Parse("application/xml"));
options.FormatterMappings.SetMediaTypeMappingForFormat(
"json", MediaTypeHeaderValue.Parse("application/json"));
options.FormatterMappings.SetMediaTypeMappingForFormat(
"xml+hateoas", MediaTypeHeaderValue.Parse("application/xml"));
options.FormatterMappings.SetMediaTypeMappingForFormat(
"json+hateoas", MediaTypeHeaderValue.Parse("application/json"));
})
.AddJsonOptions(options => {
// Force Camel Case to JSON
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters()
;
And I am using the accept header in my controller methods to differentiate between normal xml/json response, and hateoas-like response, like this:
[HttpGet]
[Route("GetAllSomething")]
public async Task<IActionResult> GetAllSomething([FromHeader(Name = "Accept")]string accept)
{
...
bool generateLinks = !string.IsNullOrWhiteSpace(accept) && accept.ToLower().EndsWith("hateoas");
...
if (generateLinks)
{
AddNavigationLink(Url.Link("GetSomethingById", new { Something.Id }), "self", "GET");
}
...
}
So, in short, I do not want to create custom formatters, because the only "custom" thing is to either include or exclude navigational links in my response, but the response itself should be xml or json based on the Accept header value.
My model class looks like this (with mainly strings and basic values in it):
[DataContract]
public class SomethingResponse
{
[DataMember]
public int Id { get; private set; }
When calling my service from Fiddler, I got the following results for the different Accept values:
Accept: application/json -> Status code 200 with only the requested data.
Accept: application/json+hateoas -> Status code 406 (Not Acceptable).
Accept: application/xml -> Status code 504. [Fiddler] ReadResponse() failed: The server did not return a complete response for this request. Server returned 468 bytes.
Accept: application/xml+hateoas -> Status code 406 (Not Acceptable).
Could someone tell me which setting is wrong?
Mapping of format to Media Type (SetMediaTypeMappingForFormat calls) works not as you expect. This mapping does not use Accept header in the request. It reads requested format from parameter named format in route data or URL query string. You should also mark your controller or action with FormatFilter attribute. There are several good articles about response formatting based on FormatFilter attribute, check here and here.
To fix your current format mappings, you should do the following:
Rename format so that it does not contain plus sign. Special + character will give you troubles when passed in URL. It's better to replace it with -:
options.FormatterMappings.SetMediaTypeMappingForFormat(
"xml-hateoas", MediaTypeHeaderValue.Parse("application/xml"));
options.FormatterMappings.SetMediaTypeMappingForFormat(
"json-hateoas", MediaTypeHeaderValue.Parse("application/json"));
Add format parameter to the route:
[Route("GetAllSomething/{format}")]
Format used for format mapping can't be extracted from Accept header, so you will pass it in the URL. Since you need to know the format for the logic in your controller, you could map above format from the route to action parameter to avoid duplication in Accept header:
public async Task<IActionResult> GetAllSomething(string format)
Now you don't need to pass required format in Accept header because the format will be mapped from request URL.
Mark controller or action with FormatFilter attribute.
The final action:
[HttpGet]
[Route("GetAllSomething/{format}")]
[FormatFilter]
public async Task<IActionResult> GetAllSomething(string format)
{
bool generateLinks = !string.IsNullOrWhiteSpace(format) && format.ToLower().EndsWith("hateoas");
// ...
return await Task.FromResult(Ok(new SomeModel { SomeProperty = "Test" }));
}
Now if you request URL /GetAllSomething/xml-hateoas (even with missing Accept header), FormatFilter will map format value of xml-hateoas to application/xml and XML formatter will be used for the response. Requested format will also be accessible in format parameter of GetAllSomething action.
Sample Project with formatter mappings on GitHub
Besides formatter mappings, you could achieve your goal by adding new supported media types to existing Media Types Formatters. Supported media types are stored in OutputFormatter.SupportedMediaTypes collection and are filled in constructor of concrete output formatter, e.g. XmlSerializerOutputFormatter. You could create the formatter instance by yourself (instead of using AddXmlSerializerFormatters extension call) and add required media types to SupportedMediaTypes collection. To adjust JSON formatter, which is added by default, just find its instance in options.OutputFormatters:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
options.InputFormatters.Add(new XmlSerializerInputFormatter());
var xmlOutputFormatter = new XmlSerializerOutputFormatter();
xmlOutputFormatter.SupportedMediaTypes.Add("application/xml+hateoas");
options.OutputFormatters.Add(xmlOutputFormatter);
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
jsonOutputFormatter?.SupportedMediaTypes.Add("application/json+hateoas");
})
.AddJsonOptions(options => {
// Force Camel Case to JSON
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.AddXmlDataContractSerializerFormatters();
}
In this case GetAllSomething should be the same as in your original question. You should also pass required format in Accept header, e.g. Accept: application/xml+hateoas.
Sample Project with custom media types on GitHub

PostAsJson could not post data

I have Web api method like below. The Web API is developed inClassic .Net 4.6.2
[HttpPost]
public async Task<IEnumerable<DocumentDTO>> GetDocuments([FromBody]IEnumerable<string> documentNames)
{
return await _domainService.GetDocuments(documentNames);
}
Then I have ASP.Net Core client that was using HttpClient to post data. I have my own extension method that serializes the input using Newtonsoft.Json.JsonConvert and post it and then de-serializes the response
public static async Task<TResult> MyPostMethodAsync<TSource, TResult>(this HttpClient httpClient, TSource source, string url)
{
// serialize the input
var content = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(source)).ConfigureAwait(false);
var stringContent = new StringContent(content, Encoding.UTF8, "application/json");
//post json string
var httpResponse = await httpClient.PostAsync(url, stringContent).ConfigureAwait(false);
//ensures ok response
httpResponse.EnsureSuccessStatusCode();
// get response string
var result = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
//de-serialize the response
return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject<TResult>(result)).ConfigureAwait(false);
}
The method above working fine. Note that its using PostAsync method.
Then I changed the above method to make use of PostAsJsonAsync extension method that is available in Microsoft.AspNet.WebApi.Client. So the new method looks like below
public static async Task<TResult> MyPostMethodAsync<TSource, TResult>(this HttpClient httpClient, TSource source, string url)
{
// post as json
var httpResponse = await httpClient.PostAsJsonAsync<TSource>(url, source).ConfigureAwait(false);
// Ensures response is okay
httpResponse.EnsureSuccessStatusCode();
// get response string
var result = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
// de-seriazlize the response
return await Task.Factory.StartNew(() => JsonConvert.DeserializeObject<TResult>(result)).ConfigureAwait(false);
}
However the PostAsJsonAsync extension method does not post any data? The Web API Method always receive empty collection for documentnames parameter. (I'm also using my extension method to POST data to additional web api methods as well, but all POST methods receives null values or empty collection)
I am guessing its serialization/deserialization issue but I am not sure which serializer .Net 4.6.2 & .Net Core uses by default.

How to upload file AND model using Web API?

Basically I'm trying to upload an image along with an enum using Web API 2.
Here's the controller signature:
[HttpPost]
public UploadResponseVm Upload([FromBody]ResImageType type)
{
The thing is, whenever I try to post a multipart form (with a file and a type) I get a 415 error:
{"Message":"The request entity's media type 'multipart/form-data' is
not supported for this resource.","ExceptionMessage":"No
MediaTypeFormatter is available to read an object of type
'ResImageType' from content with media type
'multipart/form-data'.","ExceptionType":"System.Net.Http.UnsupportedMediaTypeException","StackTrace":"
at System.Net.Http.HttpContentExtensions.ReadAsAsync[T](HttpContent
content, Type type, IEnumerable1 formatters, IFormatterLogger
formatterLogger, CancellationToken cancellationToken)\r\n at
System.Web.Http.ModelBinding.FormatterParameterBinding.ReadContentAsync(HttpRequestMessage
request, Type type, IEnumerable1 formatters, IFormatterLogger
formatterLogger, CancellationToken cancellationToken)"}
I have even added the following to my startup.cs class:
config.Formatters.Insert(0, new System.Net.Http.Formatting.JsonMediaTypeFormatter());
How can I upload a model along with a file using a web api controller?
There is no formatter that could handle/relate to your ResImageType object. I have once solved a similar problem without a formatter by using a parameter-less method and have processed the data inside the method. For example:
public async Task<HttpResponseMessage> PostFormData()
{
if (!Request.Content.IsMimeMultipartContent("form-data"))
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
string upDir= "PathOfDirectory";
MultipartFormDataStreamProvider streamProvider = new MultipartFormDataStreamProvider(upDir);
MultipartFileStreamProvider multipartFileStreamProvider = await Request.Content.ReadAsMultipartAsync(streamProvider);
// Loop through files.
foreach (MultipartFileData file in streamProvider.FileData)
{
// Save filepaths to DB or do something else
}
return Request.CreateResponse(HttpStatusCode.OK);
}
Similar solution from MS Docs
Another possibility is to create a DTO-like class that is used to transfer the object and use a formatter, for example MultipartDataMediaFormatter seems legit (haven't tried).
You can simply add media and enum at the same time by adding content type in header request.
Content-Type:application/x-www-form-urlencoded
Then try to access file and enum
[HttpPost]
public UploadResponseVm Upload()
{
//Get enum type
var enumkey = HttpContext.Current.Request.Form.AllKeys.FirstOrDefault(x => x == "enumkey");
var enumType = Convert.ToInt32(HttpContext.Current.Request.Form[enumkey]).ToEnum<ResImageType>();
//Access files
var postedFileLst = HttpContext.Current.Request.Files.GetMultiple("fileKey").ToList();
}
Use Extension Method to convert to enum
public static T ToEnum<T>(this int param)
{
var info = typeof(T);
if (info.IsEnum)
{
T result = (T)Enum.Parse(typeof(T), param.ToString(), true);
return result;
}
return default(T);
}
Also verify using curl
curl --location --request POST '{{hostname}}/api/media/addmedia' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--form 'BookLogos=#/path/to/file' \
--form 'enumkey=1'

How to set content type dynamically in a Spring MVC controller (depending on presence of request param)?

I have a REST API that until now always returned JSONP (JSON data wrapped in whatever function call client wanted):
static final String JAVASCRIPT = "application/javascript;charset=UTF-8";
#RequestMapping(value = "/matches", produces = JAVASCRIPT)
#ResponseBody
public String matches(#RequestParam String callback) {
String json = jsonService.getCachedJson("/matches");
return toJsonp(callback, json);
}
Now, things have changed so that I need to return either JSON or JSONP: if client provides a callback function name, we return JSONP and otherwise pure JSON.
With regards to content type, I'd like to be as correct as possible and use application/json for JSON and application/javascript for JSONP.
So, something like this:
#RequestMapping(value = "/matches")
#ResponseBody
public String matches(#RequestParam(required = false) String callback) {
String json = jsonService.getCachedJson("/matches");
// TODO: if callback == null, set content type to "application/json",
// otherwise to "application/javascript"
return jsonOrJsonp(callback, json);
}
String jsonOrJsonp(String callback, String json) {
return Strings.isNullOrEmpty(callback) ? json : toJsonP(callback, json);
}
Looks like I can no longer use produces attribute of #RequestMapping. What's the simplest way to set content type with Spring MVC in the scenario above?
I'd like to avoid defining HttpMessageConverters (or other Spring hassle) or changing the method return type, if at all possible! And obviously I wouldn't like duplicated method declarations where produces value is the only significant difference. What I'm looking for is minimal changes to the above code.
Latest Spring (3.2.3).
Have you tried just using two request handler methods?
#RequestMapping(value = "/matches", produces = JAVASCRIPT, params="callback")
#ResponseBody
public String Jsonp(#RequestParam String callback) {
return toJsonp(callback, jsonService.getCachedJson("/matches"));
}
#RequestMapping(value = "/matches", produces = JSON)
#ResponseBody
public String json() {
return toJson(jsonService.getCachedJson("/matches"));
}
The first method with the params parameter will only be mapped to requests where the callback param is present.

Is there a way to force ASP.NET Web API to return plain text?

I need to get a response back in plain text from a ASP.NET Web API controller.
I have tried do a request with Accept: text/plain but it doesn't seem to do the trick.
Besides, the request is external and out of my control. What I would accomplish is to mimic the old ASP.NET way:
context.Response.ContentType = "text/plain";
context.Response.Write("some text);
Any ideas?
EDIT, solution:
Based on Aliostad's answer, I added the WebAPIContrib text formatter, initialized it in the Application_Start:
config.Formatters.Add(new PlainTextFormatter());
and my controller ended up something like:
[HttpGet, HttpPost]
public HttpResponseMessage GetPlainText()
{
return ControllerContext.Request.CreateResponse(HttpStatusCode.OK, "Test data", "text/plain");
}
Hmmm... I don't think you need to create a custom formatter to make this work. Instead return the content like this:
[HttpGet]
public HttpResponseMessage HelloWorld()
{
string result = "Hello world! Time is: " + DateTime.Now;
var resp = new HttpResponseMessage(HttpStatusCode.OK);
resp.Content = new StringContent(result, System.Text.Encoding.UTF8, "text/plain");
return resp;
}
This works for me without using a custom formatter.
If you explicitly want to create output and override the default content negotiation based on Accept headers you won't want to use Request.CreateResponse() because it forces the mime type.
Instead explicitly create a new HttpResponseMessage and assign the content manually. The example above uses StringContent but there are quite a few other content classes available to return data from various .NET data types/structures.
For .net core:
[HttpGet("About")]
public ContentResult About()
{
return Content("About text");
}
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/formatting
If you are just looking for a simple plain/text formatter without adding additional dependencies, this should do the trick.
public class TextPlainFormatter : MediaTypeFormatter
{
public TextPlainFormatter()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
}
public override bool CanWriteType(Type type)
{
return type == typeof(string);
}
public override bool CanReadType(Type type)
{
return type == typeof(string);
}
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
{
return Task.Factory.StartNew(() => {
StreamWriter writer = new StreamWriter(stream);
writer.Write(value);
writer.Flush();
});
}
public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
{
return Task.Factory.StartNew(() => {
StreamReader reader = new StreamReader(stream);
return (object)reader.ReadToEnd();
});
}
}
Don't forget to add it to your Global web api config.
config.Formatters.Add(new TextPlainFormatter());
Now you can pass string objects to
this.Request.CreateResponse(HttpStatusCode.OK, "some text", "text/plain");
Please be careful not to use context in ASP.NET Web API or you will sooner or later be sorry. Asynchronous nature of ASP.NET Web API makes using HttpContext.Current a liability.
Use a plain text formatter and add to your formatters. There are dozens of them around. You could even write yours easily. WebApiContrib has one.
You can force it by setting the content type header on httpResponseMessage.Headers to text/plain in your controller provided you have registered plain text formatter.
When Accept: text/plain doesnt work, then there is no registered formatter for text mime types.
You can ensure that there is no formatters for specified mime type by getting list of all supported formatters from service configuration.
Create a very straightforward media type formatter that support text mime types.
http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters
An extension like the following one can reduce the number of lines and beautify your code:
public static class CommonExtensions
{
public static HttpResponseMessage ToHttpResponseMessage(this string str)
{
var resp = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(str, System.Text.Encoding.UTF8, "text/plain")
};
return resp;
}
}
Now you can consume the defined extension in your Web API:
public class HomeController : ApiController
{
[System.Web.Http.HttpGet]
public HttpResponseMessage Index()
{
return "Salam".ToHttpResponseMessage();
}
}
By routing {DOMAIN}/api/Home/Index you can see the following plain text:
MyPlainTextResponse

Resources