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'
Related
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");
My API is as below. I am trying to post model value wth image file using postman. But, model value did not bind. How to post json data with image file using POSTMAN?
[HttpPost]
[Route("api/images_upload")]
public IActionResult UploadImages([FromForm] Chapter chapter,IFormFile file)
{
try
{
var fileName = Path.GetFileName(file.FileName);
var absoluteFileName = fileName.Replace(fileName, Guid.NewGuid().ToString());
var extension = Path.GetExtension(fileName);
var fullFileName = string.Concat(absoluteFileName, extension);
var physicalPath = Path.Combine(HostingEnvironment.WebRootPath, "images", fullFileName);
file.CopyTo(new FileStream(physicalPath, FileMode.Create));
return Content("Images Uploaded");
}
catch (Exception ex)
{
throw ex;
}
}
For posting Chapter and IFormFile file, you could try to post with form-data in PostMan.
For posting IFormFile, you could not pass it through application/json content type. And when posting form-data, you should not add Content-Type as application/json, otherwise, the .net core will try JsonFormatter to bind the request body to model instead of FromForm, then chapter will be null.
Here is a working PostMan demo.
Note, there is no Content-Type header.
None of the above quite worked for me. But my issue was fixed by creating a new class for the type and then using the Bind attribute.
[Bind]
public class FileApplication
{
public IFormFile Files { get; set; }
public long Id { get; set; }
}
Try adding content type for the file if you know content in the file of specific type like application/pdf.
For me After adding content-type column in form data in postman and adding content-type of the file like application/xml worked
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
I want to modify HTTP request URI and HTTP request method using a CXF interceptor in a HTTP client.
I have developed something like this:
public class MyInterceptor extends AbstractPhaseInterceptor<Message> {
public MyInterceptor() {
super(Phase.PRE_PROTOCOL);
}
public void handleMessage(Message message) {
// this returns me correct path and method
// String path = (String) message.getExchange().getOutMessage().get(Message.REQUEST_URI);
// String method = (String) message.getExchange().getOutMessage().get(Message.HTTP_REQUEST_METHOD);
// this does not work as expected
String path = (String) message.get(Message.REQUEST_URI);
String method = (String) message.get(Message.HTTP_REQUEST_METHOD);
// do things here
}
}
Why do need I to use exchange/OutMessage to obtain data about current message and I can not use message directly?
How can I edit both values? I tried using message.put(<key>, <value>) and the same with exchange/OutMessage, but nothing is modified.
Coming to the path, you'd always get that value as null, I believe.
You can try following code, to get the actual value of your uri:
String requestURI = (String) message.get(Message.class.getName() + ".REQUEST_URI");
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