I came across a weird problem while working with ravenDb in a Asp.net Web api 2 project. I have a controller called "TasksController" with a route of "api/tasks". Corresponding to this I also have a tasks collection in my ravendb with Ids like "tasks/1000". When you create a new task by http-post, the api creates a new task as expected and redirects to "tasks/{id}" route. However because the Id of the newly created task is "tasks/1029" the redirect URL coming back becomes "../api/tasks/tasks/1029" which obviously does not exist. Any suggestions please?
Code
post method
[HttpPost]
[Route("")]
public async Task<IHttpActionResult> Post([FromBody]Task task)
{
task.Notes.Add(DateTime.Now.ToString("F"));
await _repositoryFactory.TaskRepository.SaveTask(task);
await _repositoryFactory.SaveChanges();
return Redirect(Url.Link("Get", new {id = task.Id}));
}
Get
[HttpGet]
[Route("{id}", Name = "Get")]
public async Task<IHttpActionResult> Get(string id)
{
var task = await _repositoryFactory.TaskRepository.GetById(id);
return Ok(task);
}
POSTMAN - Post request
POST /Hoxro.Web.Api/api/tasks/ HTTP/1.1
Host: localhost
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 86fba9d3-cfcc-4060-226b-1db0eb189c7a
{
"priority": 1,
"notes": [
"02 January 2016 20:52:59"
],
"linkedTasks": [],
"createdOn": "02/01/2016",
"assignedTo": "TaskTestUser",
"matterIds": [],
"createdBy": "TaskTestUser",
"completionDate": null,
"dueDate": null,
"completed": false
}
Result -
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost/Hoxro.Web.Api/api/tasks/tasks/1091'.",
"messageDetail": "No action was found on the controller 'Tasks' that matches the name 'tasks'."
}
Correct result should be - http://localhost/Hoxro.Web.Api/api/tasks/1091
I know this is because of the RavenDb Id format, but if I do not go with the standard format Its really difficult to load related documents or I just don't know how to?
If you want redirect to the named route you can use RedirectToRoute method instead of manually generation redirection url.
[HttpPost]
[Route("")]
public async Task<IHttpActionResult> Post([FromBody]Task task)
{
task.Notes.Add(DateTime.Now.ToString("F"));
await _repositoryFactory.TaskRepository.SaveTask(task);
await _repositoryFactory.SaveChanges();
return RedirectToRoute("Get", new {id = task.Id});
}
Also the "right way" is illustrated in web api overview where url add to the response header:
[Route("api/books")]
public HttpResponseMessage Post(Book book)
{
// Validate and add book to database (not shown)
var response = Request.CreateResponse(HttpStatusCode.Created);
// Generate a link to the new book and set the Location header in the response.
string uri = Url.Link("GetBookById", new { id = book.BookId });
response.Headers.Location = new Uri(uri);
return response;
}
Related
I can't seem to get my webapi to work in PostMan and it gives me a 404 when POSTing, but works only when using GET (even though the api specifically set to accept only POSTs! - go figure!)
Here's the controller code (that works) - NOTE: I can't use formdata as this is dotnet fw 4.72
[Route("api/GetProfile")]
[HttpPost]
public async Task<IHttpActionResult> GetProfile(string UserId)
{
var retval = new Profile();
if (UserId != null)
{
if (await dbv.IsValidUserIdAsync(UserId))
{
retval = await profile_Data.GetProfileAsync(UserId);
}
}
return Ok(retval);
}
The code works fine for GET (even though it's set to accept POST!), which it shouldn't.
In PostMan, the URI is
https://localhost:44371/api/GetProfile
The route is 100% correct!
On the Body tab, it is set to RAW and the following JSON is inside
{"UserId" : "69d40311-f9e0-4499-82ea-959949fc34fe"}
The parameter is 100% correct!
The error when attempting to POST is
{
"Message": "No HTTP resource was found that matches the request URI 'https://localhost:44371/api/GetProfile'.",
"MessageDetail": "No action was found on the controller 'Accounts' that matches the request."
}
If I put the parameters in the querystring, it works (even though the controller is set to accept POST).
If I change the controller to GET and PostMan to GET (and set the parameters in params), it works.
Is PostMan not compatible with ASP.Net webapi 2.0 ?
Why would GET work and POST not work? Makes no sense?
Try to set Content-Type application/json on postman and in your controller's POST method add the attribute FromBody
[FromBody] isn't inferred for simple types such as string or int. Therefore, the [FromBody] attribute should be used for simple types when that functionality is needed.
[Route("api/GetProfile")]
[HttpPost]
public async Task<IHttpActionResult> GetProfile([FromBody] string UserId)
{
var retval = new Profile();
if (UserId != null)
{
if (await dbv.IsValidUserIdAsync(UserId))
{
retval = await profile_Data.GetProfileAsync(UserId);
}
}
return Ok(retval);
}
Also consider to return CreatedAtAction or CreatedAtRoute instead of Ok
I have created an azure function (HTTP triggered with Open API) and deployed to azure. The endpoints working good. I am getting response when testing from Postman
Swagger UI also loading. But when trying to post from Swagger it keeps saying 401 unauthorized. But I have copied the function key from Azure portal (below screen shot) and specified that in authorize popup with in swagger.
But it still saying unauthorized.
When I copy the URL from portal for the http endpoint, it looks like this
https://myurls-asev3.appserviceenvironment.net/api/ObjectRead?code=mycode
Here mycode is exactly the same one I copied from function keys. But the only difference is, the code is attached as a query string in url when I copied the URL from portal
But in swagger it sends as header.
But in function configuration I designed it to accept as header.
[FunctionName("ObjectRead")]
[OpenApiOperation(operationId: "Run", tags: new[] { "name" })]
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "code", In = OpenApiSecurityLocationType.Header)]
[OpenApiRequestBody("application/json", typeof(FileDetails))]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string), Description = "The OK response")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req)
{
_logger.LogInformation($"ObjectRead function triggered at {DateTime.UtcNow.ToString("O")}.");
var requestBody = await new StreamReader(req.Body).ReadToEndAsync().ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<FileDetails>(requestBody);
var responseMessage = await _objectReadService.ReadAsync(data.FileName, data.FilePath).ConfigureAwait(false);
_logger.LogInformation($"ObjectRead function completed at {DateTime.UtcNow.ToString("O")}.");
return responseMessage.Length != 0 ? new FileStreamResult(responseMessage, "application/octet-stream") { FileDownloadName = data.FileName} : new NotFoundObjectResult("Unable to retrieve the file or File not found.");
}
Above code snippet 3rd line I have mentioned the key as header
Even why its not working in swagger and in url why the code still shows as query string
From the documentation:
https://<APP_NAME>.azurewebsites.net/api/<FUNCTION_NAME>?code=<API_KEY>
The key can be included in a query string variable named code, as above. It can also be included in an x-functions-key HTTP header. The value of the key can be any function key defined for the function, or any host key.
so your attirbute should looks like that:
[OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "x-functions-key", In = OpenApiSecurityLocationType.Header)]
I am creating an HTTP Partial method in my ASP.NET Web API controller and I read this document http://benfoster.io/blog/aspnet-core-json-patch-partial-api-updates on how to achieve HTTP Partial methods in a controller. I get an exception when I hit the HTTP Partial endpoint that says
Here is my code for the Patch method in the controller:
[HttpPatch("{userId}")]
public IActionResult Patch([FromRoute(Name = "userId")]Guid userId, [FromBody] JsonPatchDocument<User> userProperties)
{
var indexOfUserToPartiallyUpdate = UsersInMemory.List.FindIndex(user => user.Id == userId);
if (indexOfUserToPartiallyUpdate == -1)
{
return BadRequest($"user with {userId} not found.");
}
var originalUser = UsersInMemory.List[indexOfUserToPartiallyUpdate];
userProperties.ApplyTo(UsersInMemory.List[indexOfUserToPartiallyUpdate], ModelState);
if (!ModelState.IsValid)
{
return new BadRequestObjectResult(ModelState);
}
var model = new
{
beforePatch = originalUser,
afterPatch = UsersInMemory.List[indexOfUserToPartiallyUpdate]
};
return Ok(model);
}
And here is the JSON body I'm sending through postman in the HTTP PATCH request:
I feel like I need to do something in the Startup.cs file such as configuring the JsonPatchDocument but I don't know how. Any help is much appreciated.
I think i found your issue: "Note that we always send an array of operations even if you're only sending a single operation."
Try to change your request in:
[
{
"op": "replace",
"path": "/email",
"value": "THIS_SOME_OTHER_EMAIL#gmail.com"
}
]
I'm having trouble having a .NET Core API Controller endpoint resolve to a CSV download. I'm using the following code which I pulled from a .NET 4.5 controller:
[HttpGet]
[Route("{id:int}")]
public async Task<HttpResponseMessage> Get(int id)
{
string csv = await reportManager.GetReport(CustomerId, id);
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(csv);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
response.Content.Headers.ContentDisposition =
new ContentDispositionHeaderValue("attachment") { FileName = "report.csv" };
return response;
}
When I hit this endpoint from my Angular 4 app, I get the following response written to the browser:
{
"version": {
"major": 1,
"minor": 1,
"build": -1,
"revision": -1,
"majorRevision": -1,
"minorRevision": -1
},
"content": {
"headers": [
{
"key": "Content-Type",
"value": [
"text/csv"
]
},
{
"key": "Content-Disposition",
"value": [
"attachment; filename=11.csv"
]
}
]
},
"statusCode": 200,
"reasonPhrase": "OK",
"headers": [ ],
"requestMessage": null,
"isSuccessStatusCode": true
}
My expectation is that when I hit the endpoint, the user will be prompted to download the CSV.
I found this post on how to "export" a CSV in .NET Core. The problem is that I'm retrieving the CSV in-memory from its source (an AWS S3 bucket) whereas this code seems to only work when you have an IEnumerable<object>.
I'm wondering if my problem lies in either request or response headers, where there is something preventing the browser from retrieving a CSV from my API. Here is what I see in the browser console:
Solution: Use FileResult
This should be used if you want the client to get the "Save File" dialog box.
There are a variety to choose from here, such as FileContentResult, FileStreamResult, VirtualFileResult, PhysicalFileResult; but they all derive from FileResult - so we will go with that one for this example.
public async Task<FileResult> Download()
{
string fileName = "foo.csv";
byte[] fileBytes = ... ;
return File(fileBytes, "text/csv", fileName); // this is the key!
}
The above will also work if you use public async Task<IActionResult> if you prefer using that instead.
The key is that you return a File type.
Extra: Content-Disposition
The FileResult will automatically provide the proper Content-Disposition header to attachment.
If you want to open the file in the browser ("inline"), instead of prompting the "Save File" dialog ("attachment"). Then you can do that by changing the Content-Disposition header value.
Take for example, we want to show the PDF file in the browser.
public IActionResult Index()
{
byte[] contents = FetchPdfBytes();
Response.AddHeader("Content-Disposition", "inline; filename=test.pdf");
return File(contents, "application/pdf");
}
Credit to this SO Answer
Custom Formatters
Custom formatters are a great choice in general, because they allow the client to ask for the type they want the data as, such as the more popular JSON or the less popular XML.
This primarily works by serving the content as specified in the Accept header that the client passes to the server, such as CSV, XLS, XML, JSON, etc.
You want to use a format type of "text/csv" but there is no predefined formatter for this, so you will have to manually add it to the input and output formatter collections:
services.AddMvc(options =>
{
options.InputFormatters.Insert(0, new MyCustomInputFormatter());
options.OutputFormatters.Insert(0, new MyCustomOutputFormatter());
});
Very Simple Custom Formatter
Here's a very simple version of a custom formatter, which is a stripped-down version that was provided with the Microsoft Docs example.
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
protected override bool CanWriteType(Type type)
{
return true; // you could be fancy here but this gets the job done.
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
// your magic goes here
string foo = "Hello World!";
return response.WriteAsync(foo);
}
}
Forcing a Particular Format
// force all actions in the controller
[Produces("text/csv")]
public class FooController
{
// or apply on to a single action
[Produces("text/csv")]
public async Task<IActionResult> Index()
{
}
}
For more information, I would recommend that you read:
Introduction to formatting response data in ASP.NET Core MVC
Custom formatters in ASP.NET Core MVC
Newcomers to this question please see Svek's answer. The original question is concerning http Content-Disposition, but it looks like search engines send generic .net core csv queries here. Svek's answer provides a good overview of the tools available to .Net Core for returning CSV data from a controller.
The proper way to force a file to be downloaded instead of displayed inline is using the Content-Disposition response header. While the below solution works (see documentation) it's been pointed out that this can have unintended side effects.
Old Answer
Setting the Content-Type response header to application/octet-stream will force most major browsers to prompt the user to save the file instead of displaying it in the window.
Try doing something like this:
var result = new FileContentResult(myCsvByteArray, "application/octet-stream");
result.FileDownloadName = "my-csv-file.csv";
return result;
See my answer to this similar question for more info
I have a action method like this
[ResponseType(typeof(DiaryDeviceDTO))]
[HttpPost]
[Route("api/Device/Register")]
public async Task<IHttpActionResult> Register(DeviceRegistration deviceRegistration)
{
if (deviceRegistration == null)
{
return BadRequest("Request body is null");
}
DiaryDevice device = await _deviceBl.Register(deviceRegistration.RegistrationCode);
var deviceDto = Mapper.Map<DiaryDevice, DiaryDeviceDTO>(device);
return Ok(deviceDto);
}
When I call this api from PostMan with below request body, I get deviceRegistration object as null. I also set ContentType header as application/json
{
"ApiKey" : "apikey",
"RegistrationCode" : "123",
"ImeiNo" : "12345"
}
Then I try to read the request content as below-
string body = await Request.Content.ReadAsStringAsync();
This time I also get body = ""
But when I run my Unit test I get deviceRegistration as I wanted. So what's wrong with my code. Why my code only work for unit testing. I am using Web Api 2.2
Update / Solution
Sorry for asking this question. Actually it was my mistake. I accidentally read the request body inside Application_BeginRequest() method for logging. I move those logging codes inside Application_EndRequest() method and everything becomes ok.
Given what you've shown, this should work for the requests to api/device/register
[ResponseType(typeof(DiaryDeviceDTO))]
[HttpPost]
[Route("api/Device/Register")]
public async Task<IHttpActionResult> Register([FromBody]DeviceRegistration deviceRegistration)
{
if (deviceRegistration == null)
{
return BadRequest("Request body is null");
}
DiaryDevice device = await _deviceBl.Register(deviceRegistration.RegistrationCode);
var deviceDto = Mapper.Map<DiaryDevice, DiaryDeviceDTO>(device);
return Ok(deviceDto);
}
Note the [FromBody] attribute.