Call a controller action asynchronously in MVC - c#

I have a Controller in my MVC 5 application as follows;
public class DownloadController : Controller
{
public FileContentResult CsvDownload(string fileName)
{
byte[] data = GetCSVData(fileName);
return File(data, "text/csv", fileName);
}
}
I have some other Actions in the DownloadController as well. I want to convert the CsvDownload File result to an async Task, because sometimes the data can take time to process. How can I run the CsvDownload function asynchronously so that it does not block the main thread. At times it takes about 2 to 4 minutes to download data.
What is the preferred solution to running a method on a separate thread while in the same controller.
EDIT
I was searching for a solution when I came across the following:
http://blog.newslacker.net/2013/03/aspnet-mvc-how-to-make-working-progress.html
If I can get a solution of something like the following:
public delegate string GetCsvDataCaller(string fileName);
public string GetCsvData(string fileName)
{
string file = "filename.csv";
//Get CSV Data
return file;
}
public JsonResult StartExporting(string fileName)
{
var caller = new GetCsvDataCaller(GetCsvData);
caller.BeginInvoke(fileName, GetCsvCallBack, null);
return Json(new { Status = "started" }, JsonRequestBehavior.DenyGet);
}
public JsonResult GetCsvCallBack(IAsyncResult ar)
{
AsyncResult result = (AsyncResult) ar;
AsyncMethodCaller caller = (AsyncMethodCaller) result.AsyncDelegate;
string returnValue = caller.EndInvoke();
return Json(new { FileName = returnValue }, JsonRequestBehavior.DenyGet);
}
Is this possible in ASP.NET MVC?

public class DownloadController : Controller
{
[HttpPost]
public async Task<ActionResult> CsvDownload(string fileName)
{
byte[] data = await Task.Run(() => GetCSVData(fileName));
return File(data, "text/csv", fileName);
}
}

Related

Exported file is returning as JSON instead of Excel file

I'm trying to return an Excel file but its always trying to save a JSON file instead of Excel file. Can you tell me what is wrong with this code? I thought the content type was wrong but I tried several options there and yet still returning a JSON...
/Excel builder
public class ExcelFileBuilder
{
public byte[] BuildEmployeeRecordFile(IEnumerable<EmployeeRecordDto> records)
{
byte[] fileContents;
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
using (var package = new ExcelPackage())
{
var worksheet1 = package.Workbook.Worksheets.Add("TEST");
//ADD data here
// Export it to byte array
fileContents = package.GetAsByteArray();
}
return fileContents;
}
/Query class
public class ExportEmployeeRecordQuery : IRequest<ExportEmployeeRecordVm>
{
}
public class ExportEmployeeRecordQueryHandler : IRequestHandler<ExportEmployeeRecordQuery, ExportEmployeeRecordVm>
{
private readonly IApplicationDbContext _context;
private readonly IExcelFileBuilder _fileBuilder;
public ExportEmployeeRecordQueryHandler(IApplicationDbContext context, IExcelFileBuilder fileBuilder)
{
_context = context;
_fileBuilder = fileBuilder;
}
public async Task<ExportEmployeeRecordVm> Handle(ExportEmployeeRecordQuery request, CancellationToken cancellationToken)
{
var vm = new ExportEmployeeRecordVm();
//get data from db here
vm.Content = _fileBuilder.BuildEmployeeRecordFile(null); //pass null for now
vm.ContentType = "application/octet-stream";
vm.FileName = "TestFile.xlsx";
return await Task.FromResult(vm);
}
}
/VM
public class ExportEmployeeRecordVm
{
public string FileName { get; set; }
public string ContentType { get; set; }
public byte[] Content { get; set; }
}
/Azure function
public class ExportAgentEmployeeRecordFunction
{
private readonly IMediator _mediator;
private readonly IHttpFunctionExecutor _httpFunctionExecutor;
public ExportAgentEmployeeRecordFunction(IMediator mediator, IHttpFunctionExecutor httpFunctionExecutor)
{
_mediator = mediator;
_httpFunctionExecutor = httpFunctionExecutor;
}
[FunctionName("ExportAgentEmployeeRecordFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
ExportEmployeeRecordQuery qry)
{
return await _httpFunctionExecutor.ExecuteAsync(async () =>
{
var res = await _mediator.Send(qry);
return new OkObjectResult(res);
});
}
}
I'm trying to return an Excel file but its always trying to save a JSON file instead of Excel file. Can you tell me what's wrong with this code? I thought the content type was wrong but I tried several options there and yet still returning a JSON.
You are using the wrong derived action result type
The OkObjectResult
An ObjectResult that when executed performs content negotiation, formats the entity body, and will produce a Status200OK response if negotiation and formatting succeed
Which mean it is wrapping the passed view model and returning that as JSON based on the request.
Given what you are trying to achieve you would need to use the FileContentResult Class,
Represents an ActionResult that when executed will write a binary file to the response.
and populate it using the members of the returned view model
For example
//...
[FunctionName("ExportAgentEmployeeRecordFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
ExportEmployeeRecordQuery qry) {
return await _httpFunctionExecutor.ExecuteAsync(async () => {
ExportEmployeeRecordVm response = await _mediator.Send(qry);
FileContentResult result = new FileContentResult(response.Content, response.ContentType){
FileDownloadName = response.FileName
};
return result;
});
}
//...
You could use OfficeOpenXml library to export to an excel file.
sample code :
var data = YourDataSource ;
ExcelPackage excel = new ExcelPackage();
var workSheet = excel.Workbook.Worksheets.Add("Sheet1");
workSheet.Cells[1, 1].LoadFromCollection(data, true);
using (var memoryStream = new MemoryStream())
{
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
Response.AddHeader("content-disposition", $"attachment; filename=myfile.xlsx");
excel.SaveAs(memoryStream);
memoryStream.WriteTo(Response.OutputStream);
Response.Flush();
Response.End();
}

How to return ActionResult (file) from class that does not derive from Controller?

I have two download file methods, so I wanted to extract part which actually hits the disk to some helper/service class, but I struggle with returning that file to controller and then to user
How can I return from class that does not derives from Controller a file with that easy-to-work method from Mvc.ControllerBase.File?
public (bool Success, string ErrorMessage, IActionResult File) TryDownloadFile(string FilePath, string FriendlyName)
{
try
{
var bytes = File.ReadAllBytes(FilePath);
if (FilePath.EndsWith(".pdf"))
{
return (true, "", new FileContentResult(bytes, "application/pdf"));
}
else
{
return (true, "", ControllerBase.File(bytes, "application/octet-stream", FriendlyName));
}
}
catch (Exception ex)
{
return (false, ex.Message, null);
}
}
The error is
An object reference is required for the non-static field, method, or property 'ControllerBase.File(Stream, string, string)'
for this line:
return (true, "", ControllerBase.File(bytes, "application/octet-stream", FriendlyName));
Is there any possibility to achieve that?
ControllerBase.File is just a convenience method that creates an instance of FileContentResult for you. Here's the actual code that gets used:
new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
You can simply take that code and use it in your class, like this:
return (
true,
"",
new FileContentResult(bytes, "application/octet-stream") { FileDownloadName = FriendlyName });
If you see what ControllerBase does, you can replicate that: https://github.com/aspnet/AspNetCore/blob/c1bc210e8ebb6402ac74f4705d5748bc8e3ee544/src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs#L1120.
public virtual FileContentResult File(byte[] fileContents, string contentType, string fileDownloadName)
=> new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName };
So create a FileContentResult with your parameters and return it from the action.

Return image file from Asp Net Core WebApi [duplicate]

Problem
I want to return a file in my ASP.Net Web API Controller, but all my approaches return the HttpResponseMessage as JSON.
Code so far
public async Task<HttpResponseMessage> DownloadAsync(string id)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent({{__insert_stream_here__}});
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return response;
}
When I call this endpoint in my browser, the Web API returns the HttpResponseMessage as JSON with the HTTP Content Header set to application/json.
If this is ASP.net-Core then you are mixing web API versions. Have the action return a derived IActionResult because in your current code the framework is treating HttpResponseMessage as a model.
[Route("api/[controller]")]
public class DownloadController : Controller {
//GET api/download/12345abc
[HttpGet("{id}")]
public async Task<IActionResult> Download(string id) {
Stream stream = await {{__get_stream_based_on_id_here__}}
if(stream == null)
return NotFound(); // returns a NotFoundResult with Status404NotFound response.
return File(stream, "application/octet-stream", "{{filename.ext}}"); // returns a FileStreamResult
}
}
Note:
The framework will dispose of the stream used in this case when the response is completed. If a using statement is used, the stream will be disposed before the response has been sent and result in an exception or corrupt response.
You can return FileResult with this methods:
1: Return FileStreamResult
[HttpGet("get-file-stream/{id}"]
public async Task<FileStreamResult> DownloadAsync(string id)
{
var fileName="myfileName.txt";
var mimeType="application/....";
Stream stream = await GetFileStreamById(id);
return new FileStreamResult(stream, mimeType)
{
FileDownloadName = fileName
};
}
2: Return FileContentResult
[HttpGet("get-file-content/{id}"]
public async Task<FileContentResult> DownloadAsync(string id)
{
var fileName="myfileName.txt";
var mimeType="application/....";
byte[] fileBytes = await GetFileBytesById(id);
return new FileContentResult(fileBytes, mimeType)
{
FileDownloadName = fileName
};
}
Here is a simplistic example of streaming a file:
using System.IO;
using Microsoft.AspNetCore.Mvc;
[HttpGet("{id}")]
public async Task<FileStreamResult> Download(int id)
{
var path = "<Get the file path using the ID>";
var stream = File.OpenRead(path);
return new FileStreamResult(stream, "application/octet-stream");
}
Note:
Be sure to use FileStreamResult from Microsoft.AspNetCore.Mvc and not from System.Web.Mvc.
ASP.NET 5 WEB API & Angular 12
You can return a FileContentResult object (Blob) from the server. It'll not get automatically downloaded. You may create an anchor tag in your front-end app programmatically and set the href property to an object URL created from the Blob by the method below. Now clicking on the anchor will download the file. You can set a file name by setting the 'download' attribute to the anchor as well.
downloadFile(path: string): Observable<any> {
return this._httpClient.post(`${environment.ApiRoot}/accountVerification/downloadFile`, { path: path }, {
observe: 'response',
responseType: 'blob'
});
}
saveFile(path: string, fileName: string): void {
this._accountApprovalsService.downloadFile(path).pipe(
take(1)
).subscribe((resp) => {
let downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(resp.body);
downloadLink.setAttribute('download', fileName);
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
});
}
Backend
[HttpPost]
[Authorize(Roles = "SystemAdmin, SystemUser")]
public async Task<IActionResult> DownloadFile(FilePath model)
{
if (ModelState.IsValid)
{
try
{
var fileName = System.IO.Path.GetFileName(model.Path);
var content = await System.IO.File.ReadAllBytesAsync(model.Path);
new FileExtensionContentTypeProvider()
.TryGetContentType(fileName, out string contentType);
return File(content, contentType, fileName);
}
catch
{
return BadRequest();
}
}
return BadRequest();
}
Following is the basic example of returning file (e.g Image file) in .NET Core Web API:
<img src="#Url.Action("RenderImage", new { id = id})" alt="No Image found" />
Below is the code for returning File from controller to view. Following is Action method which will return file:
[Route("api/[controller]")]
public class DownloadController : Controller
{
//GET api/download/123
[HttpGet]
public async Task<IActionResult> RenderImage(string userId)
{
//get Image file using _fileservice from db
var result = await _fileService.getFile(userId);
if (result.byteStream == null)
return NotFound();
return File(result.byteStream, result.ContentType, result.FileName);
}
}
Note:
Our file should be first converted into byte[] and then saved in database as varbinary(max) in order to retrieve.
FileStreamResult works for me. and File is not an IActionResult. I don't know how it can work.
add
builder.Services.AddSingleton();
in
Program.cs
[HttpGet("{fileId}")]
public ActionResult GetFile(string fileId)
{
string pathToFile = "test.rar";
if (!System.IO.File.Exists(pathToFile))
{
return NotFound();
}
if(!_fileExtensionContentTypeProvider.TryGetContentType(pathToFile,
out var ContentType))
{
ContentType = "application/octet-stream";
}
var byets=System.IO.File.ReadAllBytes(pathToFile);
return File(byets, ContentType, Path.GetFileName(pathToFile));
}
}

Return file in ASP.Net Core Web API

Problem
I want to return a file in my ASP.Net Web API Controller, but all my approaches return the HttpResponseMessage as JSON.
Code so far
public async Task<HttpResponseMessage> DownloadAsync(string id)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StreamContent({{__insert_stream_here__}});
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return response;
}
When I call this endpoint in my browser, the Web API returns the HttpResponseMessage as JSON with the HTTP Content Header set to application/json.
If this is ASP.net-Core then you are mixing web API versions. Have the action return a derived IActionResult because in your current code the framework is treating HttpResponseMessage as a model.
[Route("api/[controller]")]
public class DownloadController : Controller {
//GET api/download/12345abc
[HttpGet("{id}")]
public async Task<IActionResult> Download(string id) {
Stream stream = await {{__get_stream_based_on_id_here__}}
if(stream == null)
return NotFound(); // returns a NotFoundResult with Status404NotFound response.
return File(stream, "application/octet-stream", "{{filename.ext}}"); // returns a FileStreamResult
}
}
Note:
The framework will dispose of the stream used in this case when the response is completed. If a using statement is used, the stream will be disposed before the response has been sent and result in an exception or corrupt response.
You can return FileResult with this methods:
1: Return FileStreamResult
[HttpGet("get-file-stream/{id}"]
public async Task<FileStreamResult> DownloadAsync(string id)
{
var fileName="myfileName.txt";
var mimeType="application/....";
Stream stream = await GetFileStreamById(id);
return new FileStreamResult(stream, mimeType)
{
FileDownloadName = fileName
};
}
2: Return FileContentResult
[HttpGet("get-file-content/{id}"]
public async Task<FileContentResult> DownloadAsync(string id)
{
var fileName="myfileName.txt";
var mimeType="application/....";
byte[] fileBytes = await GetFileBytesById(id);
return new FileContentResult(fileBytes, mimeType)
{
FileDownloadName = fileName
};
}
Here is a simplistic example of streaming a file:
using System.IO;
using Microsoft.AspNetCore.Mvc;
[HttpGet("{id}")]
public async Task<FileStreamResult> Download(int id)
{
var path = "<Get the file path using the ID>";
var stream = File.OpenRead(path);
return new FileStreamResult(stream, "application/octet-stream");
}
Note:
Be sure to use FileStreamResult from Microsoft.AspNetCore.Mvc and not from System.Web.Mvc.
ASP.NET 5 WEB API & Angular 12
You can return a FileContentResult object (Blob) from the server. It'll not get automatically downloaded. You may create an anchor tag in your front-end app programmatically and set the href property to an object URL created from the Blob by the method below. Now clicking on the anchor will download the file. You can set a file name by setting the 'download' attribute to the anchor as well.
downloadFile(path: string): Observable<any> {
return this._httpClient.post(`${environment.ApiRoot}/accountVerification/downloadFile`, { path: path }, {
observe: 'response',
responseType: 'blob'
});
}
saveFile(path: string, fileName: string): void {
this._accountApprovalsService.downloadFile(path).pipe(
take(1)
).subscribe((resp) => {
let downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(resp.body);
downloadLink.setAttribute('download', fileName);
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
});
}
Backend
[HttpPost]
[Authorize(Roles = "SystemAdmin, SystemUser")]
public async Task<IActionResult> DownloadFile(FilePath model)
{
if (ModelState.IsValid)
{
try
{
var fileName = System.IO.Path.GetFileName(model.Path);
var content = await System.IO.File.ReadAllBytesAsync(model.Path);
new FileExtensionContentTypeProvider()
.TryGetContentType(fileName, out string contentType);
return File(content, contentType, fileName);
}
catch
{
return BadRequest();
}
}
return BadRequest();
}
Following is the basic example of returning file (e.g Image file) in .NET Core Web API:
<img src="#Url.Action("RenderImage", new { id = id})" alt="No Image found" />
Below is the code for returning File from controller to view. Following is Action method which will return file:
[Route("api/[controller]")]
public class DownloadController : Controller
{
//GET api/download/123
[HttpGet]
public async Task<IActionResult> RenderImage(string userId)
{
//get Image file using _fileservice from db
var result = await _fileService.getFile(userId);
if (result.byteStream == null)
return NotFound();
return File(result.byteStream, result.ContentType, result.FileName);
}
}
Note:
Our file should be first converted into byte[] and then saved in database as varbinary(max) in order to retrieve.
FileStreamResult works for me. and File is not an IActionResult. I don't know how it can work.
add
builder.Services.AddSingleton();
in
Program.cs
[HttpGet("{fileId}")]
public ActionResult GetFile(string fileId)
{
string pathToFile = "test.rar";
if (!System.IO.File.Exists(pathToFile))
{
return NotFound();
}
if(!_fileExtensionContentTypeProvider.TryGetContentType(pathToFile,
out var ContentType))
{
ContentType = "application/octet-stream";
}
var byets=System.IO.File.ReadAllBytes(pathToFile);
return File(byets, ContentType, Path.GetFileName(pathToFile));
}
}

ASP HttpClient GetAsync is not responding, nor timing out

I'm creating an Instagram API client on ASP MVC using HttpClient, I'm trying to make a get request but it fails without throwing exception or responding and doesn't respond to my timeout. Here is my code:
public class InstagramService
{
private HttpClient Client = new HttpClient {
BaseAddress = new Uri("https://api.instagram.com/v1/"),
Timeout = TimeSpan.FromMilliseconds(500)
};
public async Task<InstagramUser> GetInstagramUser(long? userId = null)
{
InstagramUser User = null;
string Parameter = (userId == null) ? "self" : userId.ToString();
try {
var response = await Client.GetAsync("users/" + Parameter + "/" + GetAccessToken());
if (response.IsSuccessStatusCode)
{
User = await response.Content.ReadAsAsync<InstagramUser>();
}
}catch(Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine(e.InnerException.Message);
}
return User;
}
private string GetAccessToken()
{
return "?access_token=" + DB.config_det_sys.Single(i => i.codigo == "ACCESS_TOKEN_INSTAGRAM" && i.estado == true).Valor;
}
}
EDIT
Here I add how I call my service on the Home Controller, I will still test changing the controller to async Task
public class HomeController : Controller
{
private InstagramService IGService = new InstagramService();
public ActionResult About()
{
var apiCall = IGService.GetInstagramUser();
var model = apiCall.Result;
return View(model);
}
}
I tested on Postman trying to make the API call and it indeed worked, so where I'm failing to catch errors?
Your problem is here:
var model = apiCall.Result;
As I describe on my blog, you shouldn't block on asynchronous code. It can cause a deadlock.
Instead of Result, use await:
var model = await apiCall;
Adding to Stephen's answer, update the controller's action to be async all the way.
public class HomeController : Controller {
private InstagramService IGService = new InstagramService();
public async Task<ActionResult> About() {
var model = await IGService.GetInstagramUser();
return View(model);
}
}

Categories

Resources