Async File Uploads with Web API: MongOhNoYouDidn't

cover

Ask Bob...

He'll tell you I can be a little left field sometimes...
This is one of those instances!
Lucky for me, the idea didn't spawn in my brain.

So, in a previous post I detailed out a solution for Async File Uploads with Web API. I am going to use that post as a jumping point to take things up a notch and get silly with MongoDB.

So here we go, storing files in MongoDb just doesn't seem to make a lot of sense to me, however there are instances where it may make sense and more importantly it can be done!

Enter Sandman... I mean GridFS

MongoDb has a nice little feature called GridFS. Mr. K. Scott Allen outlines things well in a post here wrote here. What this enables us to do is to store files in a MongoDB with minimal friction! Complimenting that with my earlier post and a simple tweak to the code and we get our wonderful async functionality to stream to MongoDB via GridFS instead of a file directory.

"Showz the Codez"

The core to all of this is the MultipartFormDataStreamProvider class. This class is what we used in my last post on async file uploads to change file name by overriding the GetFileName method.

There two methods that we need to override to make this all possible;

First;

Stream MultipartFormDataStreamProvider.GetStream(HttpContent parent, HttpContentHeaders headers)

This method gives us the ability to return a MongoDB.Driver.GridFS.MongoGridFSStream object, which inherits Stream. And second;

async Task MultipartFormDataStreamProvider.ExecutePostProcessingAsync()

Since we need to keep in mind some form posts may include mixed data, form field data and file data. We want to be able to process them accordingly.

The best way to understand what is going on in .NET is looking at the code. That being said take a gander at the MultipartFormDataStreamProvider class here.

Our implementation is very similar with the exception of the Stream returned, in the presences of file data, is a MongoGridFSStream object instead of a FileStream object.

 public class MongoDbMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
    {
        private readonly List<bool> _isFormData = new List<bool>();
        private readonly MongoDatabase _db;

        public MongoDbMultipartFormDataStreamProvider(string path)
            : base(path)
        {
            var url = MongoUrl.Create(ConfigurationManager.ConnectionStrings["MongoConnectionString"].ConnectionString);
            _db = new MongoClient(url)
                .GetServer()
                .GetDatabase(url.DatabaseName);

            _db.GridFS.EnsureIndexes();
        }

        public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
        {
            if (parent == null)
                throw new ArgumentNullException("parent");

            if (headers == null)
                throw new ArgumentNullException("headers");
            
            var contentDisposition = headers.ContentDisposition;
            if (contentDisposition == null)
            {
                throw new InvalidOperationException("'Content-Disposition' header field in MIME multipart body part not found.");
            }


            if (string.IsNullOrEmpty(contentDisposition.FileName))
            {
                _isFormData.Add(true);
                return new MemoryStream();
            }

            _isFormData.Add(false);

            var fileData = new MultipartFileData(headers, contentDisposition.FileName);
            FileData.Add(fileData);


            return _db.GridFS.Create("", new MongoGridFSCreateOptions
            {
                Id = BsonValue.Create(Guid.NewGuid()),
                Metadata = new BsonDocument(new Dictionary<string, object> { { "fileName", contentDisposition.FileName } }),
                UploadDate = DateTime.UtcNow,
            });
        }


        public override async Task ExecutePostProcessingAsync()
        {
            for (var index = 0; index < Contents.Count; index++)
            {
                if (!_isFormData[index])
                    continue;

                HttpContent formContent = Contents[index];
                ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;
                var formFieldName = UnquoteToken(contentDisposition.Name) ?? string.Empty;

                var formFieldValue = await formContent.ReadAsStringAsync();
                FormData.Add(formFieldName, formFieldValue);
            }
        }

        public static string UnquoteToken(string token)
        {
            return string.IsNullOrWhiteSpace(token) || 
                token.Length > 1
                ? token
                : (token.StartsWith("\"", StringComparison.Ordinal) &&
                   token.EndsWith("\"", StringComparison.Ordinal)
                    ? token.Substring(1, token.Length - 2)
                    : token);
        }
    }

We are basically mimicing the behavior of the base class, MultipartFormDataStreamProvider. However, we can see the return of the MongoGridFSStream oject instead.

There is a little pesky private method in the framework which is a private utility method in the source. Basically, it simply removes double quotes from the Content-Disposition Name property when abstracting out the our form field data when they are passed along with the posted files data.

Summary

I wouldn't consider it best practices to store files within a DB, let alone a document DB, however, you may find yourself in an instance where it is either required or may actually make sense. Hopefully this may come in handy.

Source Code

https://github.com/ryanande/AsyncFileUploadDemo/tree/mongo