ASP.NET MVC 4 FileResult - In error - c#

I have a simple Action on a controller which returns a PDF.
Works fine.
public FileResult GetReport(string id)
{
byte[] fileBytes = _manager.GetReport(id);
string fileName = id+ ".pdf";
return File(fileBytes, MediaTypeNames.Application.Octet, fileName);
}
When the manager fails to get the report I get back null or an empty byte[].
How can I communicate to the browser that there was a problem, when the result is set to a FileResult?

I would change the return type of your method to ActionResult.
public ActionResult GetReport(string id)
{
byte[] fileBytes = _manager.GetReport(id);
if (fileBytes != null && fileBytes.Any()){
string fileName = id+ ".pdf";
return File(fileBytes, MediaTypeNames.Application.Octet, fileName);
}
else {
//do whatever you want here
return RedirectToAction("GetReportError");
}
}

The FileResult class inherits from ActionResult. So, you can define your Action like this:
public ActionResult GetReport(string id)
{
byte[] fileBytes = _manager.GetReport(id);
string fileName = id + ".pdf";
if(fileBytes == null || fileBytes.Length == 0)
return View("Error");
return File(fileBytes, MediaTypeNames.Application.Octet, fileName);
}

If you want to "communicate to the browser" that there was an error, the standard "HTTP way" is to return status code 500, especially if your request is invoked using Ajax, so that you can gracefully handle the exception.
I would suggest to simply throw an Exception when no report found for the provided id:
public FileResult GetReport(string id)
{
// could internally throw the Exception inside 'GetReport' method
byte[] fileBytes = _manager.GetReport(id);
// or...
if (fileBytes == null || !fileBytes.Any())
throw new Exception(String.Format("No report found with id {0}", id));
return File(fileBytes, MediaTypeNames.Application.Octet, fileName = id+ ".pdf");
}
Explicitly redirecting to an error page or returning a ViewResult is not the best approach in ASP.NET MVC as this is usually the role of the HandleError filter (which is applied by default) that can be easily configured to either redirect or render some View with the Exception details (while still maintaining HTTP status 500).
This is all true assuming that a failure to fetch a report is indeed considered an exception. If it's not (for example, if we expect some report to not have an available file to dump), explicitly returning a Redirect/View result is totally acceptable.

Another workaround for handling prerequisites is to split download process into two stages. First is to check preconditions in server side method which is executed as ajax/post method.
Then if these preconditions are fulfilled you can start download request (e.g. in onSuccess callback where it is checked the return value indicating fulfillment) in which (on server side) you will handle potential exceptions in a way as it was described in above posts.

Related

what data to return from a helper method when copying a file to the system

I have a helper method that copies a file to a folder on my web server.
It works, but I'm not quite sure what to return to the method that calls it.
Right now it's just returning true...because maybe it doesn't need to return anything?
Here is the helper method:
public async Task<bool> CopyFile(IFormFile profileUpload, Guid profileId)
{
string path = #"D:\ProfilePics\" + profileId;
if (!Directory.Exists(path))
{
DirectoryInfo di = Directory.CreateDirectory(path);
}
using (var fileStream = new FileStream(path, FileMode.Create))
{
await profileUpload.CopyToAsync(fileStream);
}
return true;
}
And I call it in my API Controller:
[HttpPost]
public async Task<IActionResult> PostFormData([FromForm(Name = "file")] IFormFile profileUpload, Guid profileId)
{
if (await _gamerProfile.CopyFile(profileUpload, profileId))
{
return Ok();
} else
{
return BadRequest("Please upload a valid file");
}
}
My question is, is there a better way to handle this? Is returning true or false ok or should I return another value, or maybe nothing at all?
thanks!
I think you can improve the result from the controller in case of failure.
Their is a difference between BadRequest(400) and ServerError(500).
Bad request mean their is a problem from user side, and the user doesn't have any reason to resend without changing the request.
In case of Server Error that mean their is a current problem in server, and the user can wait and try again.
Another problem is you are checking in controller that CopyFile function return true or false.
But in your code you return only true, if you have any problem you will have an exception, not a false.
You can add a try/catch in you function and return false in case of error.

ASP.Net WebApi do not return form data via Content

I don't know if it's a bug but i'm unable to get raw request on server side.
Consider following controller method:
[AllowAnonymous]
[Route("api/sayHello")]
[HttpPost]
public string SayHello([FromBody] string userName)
{
return $"Hello, {userName}.";
}
I call it via cUrl:
curl -X POST 'https://localhost:809/api/sayHello' --insecure -d "=userName"
It works fine.
Now I'm trying to add some logging. I add a global filter which is doing following:
public async Task LogFilterAction(HttpActionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
LogFilterAction(context.ActionDescriptor?.ControllerDescriptor?.ControllerType,
context.ActionDescriptor?.ActionName,
context.Request?.RequestUri,
await GetDataAsString(context.Request?.Content),
context.Response?.StatusCode
);
}
private static async Task<string> GetDataAsString(HttpContent content)
{
if (content == null)
return null;
var contentBytes = await content.ReadAsByteArrayAsync();
return Encoding.UTF8.GetString(contentBytes);
}
But here is the problem: for unknown reason reason contentBytes are always an empty array. I see that it's actual length is 9 (it's length of =userName string)
Or Even
As you can see, ASP.Net has successfully request arguments, however, it doesn't return it's in raw manner. Stream has position=0, contentConsumed=false, and everything else is just fine. But i can't read data passed to the controller.
What's wrong here?
ASP.NET Web API reads the content only once, so at the time that you access the content stream, it has already been read and the stream is positioned at its end. Another attempt to read the content will return nothing.
However, in a small sample I was able to reset the stream and read it again:
private async Task<string> GetDataAsString(HttpContent content)
{
if (content == null)
return null;
using (var str = await content.ReadAsStreamAsync())
{
if (str.CanSeek)
str.Seek(0, System.IO.SeekOrigin.Begin);
using (var rdr = new StreamReader(str))
{
return rdr.ReadToEnd();
}
}
}
However, in order to avoid side effects, you might want to consider to use the ActionArguments property of the ActionContext. You can use this property to retrieve the values that are handed to the action for logging. This does not interfere with the internal plumbing of ASP.NET Web API.

AspNet MVC - return RedirectToAction is not working with image upload (if file.SaveAs gets called)

return RedirectToAction is not working if imageUpload.SaveAs(path); executed otherwise it does working (I mean if I do not select any image it will not reach the line imageUpload.SaveAs(path);).
Here is my code:
[HttpPost]
public ActionResult CreateNewEmployee(Employee emplView, HttpPostedFileBase imageUpload)
{
if (!ModelState.IsValid)
{
var mod = new PersonalDetailsViewModel(emplView);
return View("AddEmployee", mod);
}
if (imageUpload != null && imageUpload.ContentLength > 0)
{
var fileName = emplView.EmployeeId + "_" + Path.GetFileName(imageUpload.FileName);
var path = Path.Combine(Server.MapPath("~/Content/Images/"), fileName);
imageUpload.SaveAs(path);
emplView.Photograph = fileName;
}
_dbContext.Employees.Add(emplView);
_dbContext.SaveChanges();
return RedirectToAction("PersonalDetails", new { id = emplView.EmployeeId });
}
I checked with debugger, it is reaching to the last line and executing but not redirecting to the action method. I don't know where I am doing wrong?
I checked within following method of Global.asax for any hidden error, but there wasn't any:
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
}
Perhaps move your:
imageUpload.SaveAs(path);
to an aSync method allowing the rest of the code to execute without waiting for the image upload process to finish. I am suspecting you are getting collusion for the image upload request while trying to give a response from your controller.

How do I receive both json and binary data in a post to my webapi controller?

I have a class I created called Event. My Event class contains a list of Documents. Document is another class I've created. Each Document class contains a class called DocumentData which has a property called FileData of type byte[].
So it looks like this:
myEventObj.Documents[0].DocumentData.FileData
Right now my controller that accepts new or updated Events only accepts json data, which is fine for everything in my Event class except for FileData.
I have another controller which accepts file uploads. The problem is, there's no easy way for me to link the binary file data accepted by my second controller and pair it with the FileData property of an Event sent to my first controller. How do I combine them so there's only one call to my web service to save both an Event along with any file data?
Here's my Post and Put functions of my Event controller:
[HttpPost]
public IHttpActionResult Post(Event coreEvent) {
coreEvent.PrepareToPersist();
_unitOfWork.Events.Add(coreEvent);
_unitOfWork.Complete();
return Created("api/events/" + coreEvent.EventId, coreEvent);
}
[HttpPut]
public IHttpActionResult Put(Event coreEvent) {
coreEvent.PrepareToPersist();
_unitOfWork.Events.Update(coreEvent);
_unitOfWork.Complete();
return Ok(coreEvent);
}
Here's the Post function for my controller that handles file uploads:
[HttpPost]
public async Task<IHttpActionResult> Post() {
if (!Request.Content.IsMimeMultipartContent()) {
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = new MultipartMemoryStreamProvider();
try {
await Request.Content.ReadAsMultipartAsync(provider);
foreach (var file in provider.Contents) {
string filename = file.Headers.ContentDisposition.FileName.Trim('\"');
byte[] buffer = await file.ReadAsByteArrayAsync();
}
return Ok();
}
catch (System.Exception e) {
return InternalServerError(e);
}
}
How do I put these together?
Well usually I prefer it in two methods to support multi ajax upload files, so the second method will support uploading files by Ajax and will return a unique key for the uploaded file, after you saving it somewhere on the server and adding a record for it in the database ( ex: attachments table),
and when you call the first method for posing event , your object myEventObj.Documents[0].DocumentData.FileData will contains the returned key from the second method, so probably your object will be, myEventObj.Documents as List of string (files keys)
Thanks

MVC + returning different PartialView on condition

Not sure if this is the best approach in MVC but how do I return views on condition, let's say if I want to return another view which displays some error message if my 'fbUID' is missing, please kindly assist. Thanks.
public PartialViewResult GetCredentials(string facebookUID, string facebookAccessTok)
{
string fbUID = facebookUID;
if (fbUID != null)
{
// Request fb profile pic
var rawImg = new Bitmap(ImageHelper.requestBitmapImage(fbUID));
var processblurredImg = new Bitmap(rawImg);
var gb = new GaussianBlur();
for (int i = 0; i < 8; i++)
{
gb.ApplyInPlace(processblurredImg);
}
// Download it to local drive / server
string uploadPath = Server.MapPath("~/upload");
string fullPath = uploadPath + "\\ProfilePic.png";
if (!Directory.Exists(uploadPath))
{
Directory.CreateDirectory(uploadPath);
}
if (uploadPath != null)
{
ImageHelper.savePng(fullPath, processblurredImg, 500L);
}
return PartialView("BlurredPhoto");
}
return PartialView("TestPartialView"); //if fbUID is null
}
Have a look at action filters. These allow you to install a class via an attribute on your controller method which intercepts the call before your method runs. You can do this kind of basic checking here and return a standard error handler result from here.
ASP.NET MVC has a built-in HandleErrorFilterAttribute that helps you to return error views if some errors occured in action or other filters. The built-in HandleError filter returns view not a partial view so you may have to create a custom one to return a partial view. The idea is you have to throw some custom exception from your action if fbUID is null and the custom handle error filter returns a partial view if it handles that exception.
I suggest going for a custom handle error filter approach only if you see this functionality in many places else it's more work for a simple thing!

Categories