Notify the View after async Task is completed in MVC - c#

I have a view and I send data through ajax to an action in MVC. I want to be notified in the view only when the task is done. The problem I encounter is whether I'm notified before or if i wait untill the the task is done my view is blocked. Here it is un example of what i do:
function UploadFile(file) {
$.ajax({
url: '/Home/Upload',
type: 'POST',
data: file,
success: function (result) {
alert(result);
}
});
}
[HttpPost]
public async Task<ActionResult> Upload(httpPostedFileBase file)
{
NewFile service = new NewFile((long)Session["UserId"]);
Thread thread = new Thread(() =>service.UploadFile(file));
thread.Start();
return Json("File Uploaded", JsonRequestBehavior.AllowGet);
}
public async Task<string> UploadFile(HttpPostedFileBase file) // returns the file Id in Google Drive
{
Google.Apis.Drive.v3.Data.File uploadedFile = new Google.Apis.Drive.v3.Data.File();
try
{
if (file != null && file.ContentLength > 0)
{
// Check for credentials
Google.Apis.Drive.v3.DriveService service = Service;
// Uploads the file to the server in "~/GoogleDriveFiles" folder.
/*string path = Path.Combine(HttpContext.Current.Server.MapPath("~/GoogleDriveFiles"),*/ // TODO Doesn't work with Unit testing. Must be written manual for testing
//Path.GetFileName(file.FileName));
//file.SaveAs(path);
// Save the metadata of the file
var FileMetaData = new Google.Apis.Drive.v3.Data.File()
{
Name = Path.GetFileName(file.FileName),
MimeType = MimeMapping.GetMimeMapping(file.FileName),
Parents = new List<string>
{
GetReposFolderId()
}
};
// Create a request for upload
Google.Apis.Drive.v3.FilesResource.CreateMediaUpload request;
file.InputStream.Close();
// Create a stream using the file filepath and filling the request. Then upload.
using (var stream = new System.IO.FileStream(file.FileName, System.IO.FileMode.Open))
{
request = service.Files.Create(FileMetaData, stream, FileMetaData.MimeType);
request.Fields = "id";
await request.UploadAsync();//TODO Tuka bavi i ne mozesh da browswash prez papkite dokato ne go kachi na servara
}
uploadedFile = request.ResponseBody;
}
}
catch (Exception ex)
{
ErrorEngine.RuntimeExceptions runtimeExceptions = new ErrorEngine.RuntimeExceptions();
runtimeExceptions.ManageException(ex, this);
return ex.ToString();
}
return uploadedFile.Id;
}
I'm not very experienced with MVC and codding in general as you might thought. Can you help me with that please?

the short answer is you cant ....
if you do not want to wait for the processing... then it will never know...
UNLESS
either, you use a poller and ask every x time or use a two way comms.... which is quite a bit of coding but you could look at SingalR, then you could issue a msg and all listens can receive.
so the easiest.... for you... is to upload the file as one request and make that wait for the response.
Uploading and the action as to what to do can be separated, that's what many sites do.. you can use js, to have a progress bar as to how far it is with uploading the file to the server..but processing o the server unless you wait, there would be no way other than what i said above or variations there of.
options are poller or socket base tech, or simply wait for upload to and processing to complete.
The uploading and the processing can be separate calls with different responce and this is what most sites do...

Try this:
[HttpPost]
public async Task<JsonResult> Upload(httpPostedFileBase file)
{
try {
NewFile service = new NewFile((long)Session["UserId"]);
await Task.Run(() => service.UploadFile(file));
} catch (Exception e) {
Console.WriteLine(e);
}
return Json("File Uploaded", JsonRequestBehavior.AllowGet);
}

Related

How to upload large files to Azure blob storage in background and responding 202 accepted right away?

I have a file upload webapi endpoint where it accept IFormFile . I want to upload large files that are any where 100Mbs to GBs to Azure blob storage. I want upload the file in background and return 202 accepted as soon as I see the length of the file is greater than some threshold.
I have the following controller and injected service code:
[HttpPost]
public async Task<IActionResult> UploadFilesAsync(IFormFile uploadedFile, CancellationToken cancellationToken = default)
{
// some other code . . . .
if (uploadedFile.Length > _appConfig.Value.Threshold)
result = await _fileService.UploadFileAsync(uploadedFile, fileDataType, cancellationToken);
//map result and more code . . .
return CreatedAtRoute(nameof(GetFileAsync), new { fileId = result.FileId }, mappedDto);
}
public async Task<FileUploadResult> UploadFileAsync(IFormFile uploadedFile,CancellationToken cancellationToken)
{
var fileUploadResult = new fileUploadResult( . . .)
_ = System.Threading.Tasks.Task.Run(async () =>
{
var processResult = await _blobStorage.SaveFileAsync(uploadedFile,cancellationToken);
// upload is completed, update FileEntity status
var processStatus = processResult.HasError ? ProcessStatus.Failed : ProcessStatus.Succeeded;
await _fileRepository.UpdateFileEntityAsync(blobFileInfo, processStatus, cancellationToken);
}, cancellationToken);
return fileUploadResult ;
}
I have tried Task.Run but I still notice that the api still hangs when uploading using postman and I also learned that Task.Run is not recommended. What can I use in .net6 to trigger upload process in background and respond with 202Accepted ?
We can use background service, Coravel queueing services and signalr to implement it.
Create a jobId in upload method, add pass it to background job method.
var processResult = await _blobStorage.SaveFileAsync(uploadedFile,cancellationToken);
// check the upload result
// sample code, not test
public Dictionary jobDic = new Dictionary<string_jobid, string_status>();
private async Task PerformBackgroundJob(string jobId)
{
while(string_status=="uploading"){
// TODO: report progress with SignalR
await _hubContext.Clients.Group(jobId).SendAsync("progress", i);
await Task.Delay(100);
}
}
When the file uploaded, then we can get upload success message, then we can call frontend code or call RedirectToAction.
Related Link:
1. Communicate the status of a background job with SignalR
2. Repo

ContinueWith doesn't work in Controller to log

I can't find a solution to the problem despite many similar questions.
There is a Web API. On POST I need
read DB
make a HTTP call to other service to subscribe on notification (let's say it takes 5s)
return the data from the DB
In the step 2, I don't need to wait, I don't need to block the client (for 5sec), so the client should not wait for the response.
However, the server have to wait on result from 2 and log it. So far I've tried
[HttpPost("{callId}")]
public async Task<IActionResult> CreateSubs([FromRoute] string callId)
{
var data = await ...// read the DB
_ = SubscribeForUpdates(callId);
return Ok(data);
}
private async Task SubscribeForUpdates(string callId)
{
_logger.LogInformation("Subscribe client {ConnectionId} notifications", callId);
var requestMessage = new HttpRequestMessage
{
RequestUri = new Uri(_httpClient.BaseAddress, $"subscribe/{callId}"),
Method = HttpMethod.Get,
};
var result = await SendAsync<SubscriptionResponse>(requestMessage);
if (result.IsSuccess)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine("Fail");
}
}
SendAsync is from some library and so smth like _httpClient.SendAsync
In this case the request will not be blocked, the internal HTTP request is successful but I there is no Success from Console.WriteLine("Success");. Only if I put a breakpoint there it logs.
Could you please help me to understand why this is not log and how to fix that?
I've tried ContinueWith - no result
await SendAsync<ServerSubscriptionResponse>(requestMessage)
.ContinueWith(t =>
{
if (t.Result.IsSuccess)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine("Fail");
}
})
When I use await SubscribeForUpdates(callId) inasted of _ = SubscribeForUpdates(callId) it works and logs but the blocks a client. I need to avoid that

Asp.Net Core Web API returns the result while client is hanging

I have a simple Asp.Net Core Web API with .NET 6, running on IIS 10, Windows 10. The web API calls another API and returns the results. Below is a simplified version of its code but I tried to keep the most important parts.
[ApiController]
[Produces("application/json")]
public class SomeController
{
private async Task<ApiOutput> RunApiForClientAsync(ApiInput input)
{
try
{
//create a httpclient with a lot of configuration
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
return new ApiOutput
{
Data = content,
Error = null,
StatusCode = 200,
Input = input,
};
}
catch(Exception ex)
{
return new ApiOutput
{
Data = null,
Error = new ApiError("Error Getting the Result from the Server", ex.Message),
StatusCode = 400,
Input = input,
};
}
}
private async Task<List<ApiOutput>> RunApiCallsAsync(string requestId, IEnumerable<ApiInput> items)
{
var result = new List<ApiOutput>();
var tasks = new List<Task<ApiOutput>>();
foreach(var item in items)
{
tasks.Add(RunApiForAsync(item));
}
var taskResults = await Task.WhenAll(tasks);
result.AddRange(taskResults);
return result;
}
[HttpPost]
[Route("rest/multiple")]
public async Task<IActionResult> PostMultiple(ApiInput[] models, string? requestId)
{
_logger.LogInformation(ApiLoggingEvents.PostMultiple, "Request received with ID {requestId}", requestId);
var result = await RunApiCallsAsync(requestId, models);
try
{
_logger.LogDebug(ApiLoggingEvents.PostMultiple, "Request ID {requestId} Generating JSONs.", requestId);
var resultJson = GetJson(result);
await SaveResultAsync(resultJson, requestId);
_logger.LogDebug(ApiLoggingEvents.PostMultiple, "Request ID {requestId} Everything is finished. Returning....", requestId);
return Content(resultJson, "application/json");
}
catch (Exception ex)
{
_logger.LogDebug(ApiLoggingEvents.PostMultiple, "Exception while returning {requestId}, message {msg}", requestId, ex.Message);
throw new Exception("Try again");
}
}
}
Every once in a while, the caller sends the request to the API but never gets the result back. However, when I read the logs, I see the last line for the request is the line containing the text "Everything is finished. Returning" which means everything was successful. In addition, the output JSON is saved on the server's local drive (the await SaveResultAsync(resultJson, requestId); call is successful too).
I should mention that these types of requests are the ones that take a long while to respond. Usually around 10 minutes. Is there a setting that I need to change on the application or the IIS?
I tried to use the following but it doesn't work with the In-Process model:
builder.WebHost.UseKestrel(o =>
{
o.Limits.MaxConcurrentConnections = 100;
o.Limits.KeepAliveTimeout = TimeSpan.FromMilliseconds(timeout);
o.Limits.MaxRequestBodySize = int.MaxValue;
o.Limits.MaxResponseBufferSize = int.MaxValue;
});
Note:
The requestId is a unique GUID for every request to help me keep track of each request on the log file and see whether it was successful or not and if it has created the output file.
Update:
Upon further investigation, it seems like the requests that have a runtime more than 5 minutes are failing. Any idea what might be related to this number?
Update 2:
I created a very simple endpoint that waits for a specified amount of seconds, then returns back with a simple message:
[HttpPost]
[Route("rest/testpost")]
public IActionResult TestPost(int delay)
{
_logger.LogInformation(1, "Delay for {delay} started.", delay);
Thread.Sleep(delay * 1000);
_logger.LogInformation(1, "Delay for {delay} ended.", delay);
return Ok($"Delay for {delay} worked.");
}
I then added the requestTimeout="00:20:00" to the web.config file, just to make sure.
Interestingly, for values such as 310 seconds, sometimes I get the result, but sometimes I don't. (Postman still hangs)
To your web.config, add the following (the value is in seconds--so this will allow runtimes up to 20 minutes):
<system.web>
<httpRuntime executionTimeout="1200" />
</system.web>
Here is a link to the documentation: https://learn.microsoft.com/en-us/dotnet/api/system.web.configuration.httpruntimesection.executiontimeout?view=netframework-4.8

Return a file and a string from Controller to view in ASP.NET MVC

I was wondering if it's possible to send a string to the client and at the same time a file to the client. My purpose it is that if file has been sent to the client, it has also to show up a message of 'file downloaded successfully" like a mix of the following codes that belong to controllers:
return File(files, "application/octet-stream", "FNVBA.zip");
vm.OutputMessage = OutputMessages.SuccessDownload;
return RedirectToAction("Crea", "Spedizione", vm);
If that's possible only through AJAX, is there some sample to follow? It means I need to send the file to the AJAX request.
The approach I used for this was to send the file as a byte array in a json object, and include the filename in the object as well.
//Snippet from controller:
try
{
var imageFile = await _imageStore.GetImageFile(gId);
var imageObject = new RetrievedImageObject();
imageObject.InitializeFile(imageFile);
imageObject.Info = await _infoStorageService.GetInfo(gId);
return new JsonResult (imageObject);
}
// Snippet from ImageObject. The filename is derived from the id:
public void InitializeFile (RetrievedImageFile f)
{
DocType = f.DocType;
Id = f.Id;
using (var memStream = new MemoryStream())
{
f.FileContentStream.CopyTo(memStream);
FileContentByteArray = memStream.ToArray();
}
}

ASP.Net Asynchronous HTTP File Upload Handler

I'm trying to make a file upload handler in C# that is asynchronous and can provide updates on progress of the file through AJAX asynchronous requests. Basically if the request is a POST it loads some information into the session and then starts the upload, if the request was a GET it returns the current state of the upload (bytes uploaded, total bytes, etc). I'm not entire sure that it needs to be an asynchronous handler but the files could be quite large so I thought that would work best. For the base async handler I used something very similar to the handler in this MSDN article. I've posted below some key sections of my code below. The issue I'm having is that I don't receive any of the GET information back until the POST has completed. I will mention that in this example I am using jQuery for GET requests and BlueImp for posting the file.
The HTML and JavaScript
<input id="somefile" type="file" />
$(function () {
name = 'MyUniqueId130';
var int = null;
$('#somefile').fileupload({
url: '/fileupload.axd?key='+name,
done: function (e, data) { clearInterval(int); }
});
$('#somefile').ajaxStart(function(){
int = setInterval(function(){
$.ajax({
url: '/fileupload.axd?key='+name,
dataType: 'json',
async: true
})
.done(function(e1, data1){
if(!e1.InProgress || e1.Complete || e1.Canceled)
clearInterval(int);
});
}, 10000)});
});
The Asynchronous Process Request Method just calls the correct method whether it's a POST or GET to one of the following then calls CompleteRequest to end the request:
private static void GetFilesStatuses(HttpContext context)
{
string key = context.Request.QueryString["key"];
//A dictionary of <string, UploadStatus> in the session
var Statuses = GetSessionStore(context);
UploadStatus ups;
if (!String.IsNullOrWhiteSpace(key))
{
if (Statuses.TryGetValue(key, out ups))
{
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.Write(CreateJson(ups));
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.Write(CreateJson(Statuses.Values));
}
}
private static void UploadFile(HttpContext context)
{
var Statuses = GetSessionStore(context);
string key = context.Request.QueryString["key"];
if (String.IsNullOrWhiteSpace(key))
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
HttpPostedFile file = context.Request.Files[0];
string extn = file.FileName.LastIndexOf('.') == -1 ? "" :
file.FileName.Substring(file.FileName.LastIndexOf('.'), (file.FileName.Length - file.FileName.LastIndexOf('.')));
string temp = GetTempFileName(path, extn);
UploadStatus status = new UploadStatus()
{
FileName = file.FileName,
TempFileName = temp,
Path = path,
Complete = false,
Canceled = false,
InProgress = false,
Success = true,
BytesLoaded = 0,
TotalBytes = file.ContentLength
};
Statuses.Add(key, status);
byte[] buffer = new byte[bufferSize];
int byteCount = 0;
using (var fStream = System.IO.File.OpenWrite(context.Request.MapPath(path + temp)))
{
uploads.Add(status);
while ((byteCount = file.InputStream.Read(buffer, 0, bufferSize)) > 0 && !status.Canceled)
{
status.InProgress = true;
status.BytesLoaded += byteCount;
fStream.Write(buffer, 0, byteCount);
}
status.Complete = !status.Canceled;
status.InProgress = false;
status.Success = true;
if (status.Canceled)
{
Statuses.Remove(temp);
}
context.Response.StatusCode = (int)HttpStatusCode.OK;
}
}
I've tried many things such as non-async handlers, async handlers, making sure the JavaScript is runnning async, but at this point I think I need some different eyes on the problem so thank you for any assistance anyone can provide.
I assume you're using the default ASP.Net Session manager and I see that you call GetSessionStore to get your session. Unfortunately the default Session manager serializes all requests when a call requires write access to the Session Store. This StackOverflow question and this MSDN arcle on Session State have some very useful information on Session State and it's locking behaviors.
Now, To take care of your problem, you're going to have to do a couple things which depend on whether you're using MVC controllers or if you're writing a custom IHttpHandler.
If you're writing your own IHttpHandler, make sure you do not have the IRequiresSessionState or IReadOnlySessionState interfaces added to your handler. In doing so, the pipeline will skip looking for a session and go straight to processing. context.Session will be null in this situation.
If you're using MVC to process the request, you'll need to decorate your controller class with the SessionState attribute passing in the SessionStateBehavior of SessionStateBehavior.Disabled.
In either case you won't be able to rely on the Session object to store your upload statuses. You can create a static ConcurrentDictionary keyed off of their SessionID (which you'll either need to pass in the upload query string or read the cookie yourself, calling Session.SessionId will just block you again) and store your upload statuses in there (which look like they're Concurrent* as well).
Another option would be to replace the SessionStateProvider with your own custom provider but that might be overkill in this situation.

Categories