This came from a need to offer robust, standardized file uploads in Azure blob storage, with an emphasis on large file support. It's pretty easy to implement blob storage uploads on small files. In my experience, though, things get weird when you need to support large files -- meaning files large enough to timeout on a single request. I use and love DropzoneJS because, among other things, it supports chunking out of the box. But as a JS/frontend library, it provides no guidance how to implement upload chunking on the backend. This is just the nature of frontend libraries, not a shortcoming. So, in this repo, I worked through the problem of implementing chunked uploads to blob storage. And while I love Azure and blob storage, I think it has a discoverability problem when it comes to advanced functionality. So, if this functionality is obvious to everyone, congratulations! It was not obvious to me.
Here's my approach:
-
The heart of my solution is an interface IBlockUploader. Chunked uploads to blob storage revolve around
staging
andcommitting
blob "blocks." Chunks arestaged
over many successive POSTs and thencommitted
at the end. These interface methods will ultimately be executed by Azure's StageBlockAsync and CommitBlockListAsyncBlockBlobClient
methods. -
Create a service class BlockBlobUploader that implements
IBlockUploader
. This is what would be added to the service collection during Startup. When you upload blob chunks or "blocks", you have to track the blockIds you've created over all of yourstaging
calls. For that purpose, I made an abstract class BlockTracker. It's abstract because I could see different ways to track those blockIds -- in a database or in a local file. For this example, I opted for a simpleApp_Data
local file approach in AppDataBlockTracker. Note thatAppDataBlockTracker
didn't work for multiple concurrent uploads, so I created a database solution DbBlockTracker that does. This is not part of the package because I didn't want to add a database dependency, but my implementation here will give you some ideas on how to implement in your app. -
In my application performing the uploads, I have a FilesController. You can see it has a
BlockBlobUploader
dependency along with two actions Stage and Commit. -
My upload Razor page is here, and the important parts are here. This page has a lot of junk in it from prior iterations. The part I've highlighted is the most relevant thing to see here. There are two forms and an upload button. One form is
staging
-- the hidden form is forcommitting
. -
My Dropzone JS implementation is here.
Here's a walkthrough on this solution in PowerPoint.
A few things have changed since I made this video, but it's mostly pretty good for understanding what this does. The main change now is that in order to support multiple concurrent uploads, my Files/Commit
handler doesn't redirect anymore. A redirect durring the Commit
step would interrupt other downloads in progress. Therefore, I use an ajax fetch during Commit
so that the Upload page is allowed to keep running.
This SO answer gave me the idea for what became my BlockBlobUploader
class. The answer here used a local file example. I knew this would have to be heavily reworked to be stateless for web use. This is what led to my BlockTracker
abstract class.
Vadim17 contributed a streaming upload example in this repo. Vadim does amazing work. See his Upwork profile. But I ended up not using his implementation because it didn't integrate with DropzoneJS. I like Dropzone's user experience with the animations, drag and drop, etc.
Another service class offered in this package is AppDataTempFileProvider which implements ITempFileProvider. This came up because I needed a way to upload to blob storage from a byte array without using MemoryStream
, which is problematic for large files. The use is in a closed-source project, so I don't have an example or test built up I can show directly. But to use this, inject ITempFileProvider
into your controller or page, and use it instead of a MemoryStream
. This will save the byte array as a temp file in the App_Data
folder. Then you can perform any operation on it (typically uploading to blob storage), and be assured that the temp file will be deleted when the operation completes.
So instead of this:
using (var stream = new MemoryStream(byteArray))
{
await blobClient.UploadAsync(stream);
}
Do this:
// injected from ctor
private readonly ITempFileProvider _tempFile;
// then for your upload
await _tempFile.ExecuteAsync(byteArray, async (stream) => await blobClient.UploadAsync(stream));