ASP.Net Asynchronous HTTP File Upload Handler

2020-06-05 01:50发布

问题:

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.

回答1:

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.