MongoDB in .NET part 9: storing files

Introduction

In the previous post we looked at a couple of query techniques in the MongoDb C# driver. Before that in this series we saw how to store, update, delete and query “usual” objects like Car. Storing files, such as a PDF or Word file, on the other hand is a different matter. They take up a lot of space in the data store and require special data types to store the bytes.

In this post we’ll look at how files can be stored in MongoDb using its GridFS technology. GridFS stores files in MongoDb documents just like normal objects. However, it stores the file contents in chunks of 256KB. Each chunk is stored in a MongoDb document. Recall that a single MongoDocument can store 16MB of data. Below 16MB you can still opt for storing the byte array contents of a file in a single MongoDb document but for consistency you probably should store all your files in GridFS. GridFS also stores the file metadata in a separate MongoDocument with appropriate links to the constituent chunks.

GridFS is similar to a file system such as the Windows file directory. However, it is independent of the platform it’s running on, i.e. GridFS is not constrained by any limitations of the file system of the OS.

Also, as each chunk is stored in a MongoDb document, GridFS prepares the way for content replication in a Replica Set scenario.

Atomic operations are not available for files in GridFS. You cannot locate and update a file in a single step. Also, the only way to update a file is by replacing an existing one. You cannot send an Update document to the Mongo server to update a small part of a file. So GridFS is probably a good option for files that don’t change too often.

GridFS in the C# driver

GridFS is represented by the GridFS property of MongoDatabase. The GridFS instance with the default settings can be reached in our demo CarRental demo as follows:

MongoGridFS gridFsDefault = CarRentalContext.CarRentalDatabase.GridFS;

You can use the GetGridFS method of MongoDatabase to override the default settings:

MongoGridFSSettings gridFsSettings = new MongoGridFSSettings();
gridFsSettings.ChunkSize = 1024;
MongoGridFS gridFsCustom = CarRentalContext.CarRentalDatabase.GetGridFS(gridFsSettings);

The files can be retrieved using the Files property of MongoGridFS. By default it returns a collection of BsonDocuments but it can be serialised into MongoGridFSFileInfo objects. They allow us to easily extract the metadata about a file in an object oriented way: file name, size in bytes, date uploaded, content type etc. As the files stored in GridFS are independent of the local file system many of these properties can be provided by the user, such as the file name or a list of aliases.

If you ever wish to view individual chunks of a file then you can retrieve them using the Chunks property:

MongoCollection<BsonDocument> chunks = gridFsDefault.Chunks;

These cannot be serialised into any strongly types object yet. Each BsonDocument has a files_id field by which you can identify the chunks belonging to a single file. You can get the file id from the MongoGridFSFileInfo object. You can put the chunks into the right order using the numeric ‘n’ field which denotes the place of a chunk in the sequence. The binary data can be extracted using the ‘data’ field.

We can upload files using one of the overloaded Upload functions of MongoGridFS. One such overload allows us to specify a Stream, a remote file name – to change the file name representation in the metadata – and a MongoGridFSCreateOptions object. This last parameter will let us further define the options for uploading a single file: a list of aliases, chunk size, content type, custom metadata, upload date and an id. The Upload function returns a MongoGridFSFileInfo object.

Another way to upload files is to use the Create and CreateText methods of MongoDatabase.

Demo

We’ll extend our CarRental demo. The first goal is to be able to upload an image linked to a Car. Locate Index.cshtml in the Views/Cars folder. It contains a set of links for each Car entry: Edit, Details and Delete. Add the Image action link to that list:

<td>
        @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
        @Html.ActionLink("Details", "Details", new { id=item.Id }) |
        @Html.ActionLink("Delete", "Delete", new { id=item.Id }) |
       	@Html.ActionLink("Image", "Image", new { id=item.Id })
</td>

Add the following method to CarsController.cs:

public ActionResult Image(string id)
{
	Car car = CarRentalContext.Cars.FindOneById(new ObjectId(id));
	return View(car.ConvertToViewModel());
}

Right-click “Image” and select Add View…:

Add car rental image view

You’ll get an empty cshtml file. We’ll fill it out in a little bit. We’ll certainly need a POST action in CarsController that accepts the posted file and the Car id. Also, we’ll need a property on the Car object to store the image id. OK, let’s start from the bottom. Add the following property to Car:

public string ImageId { get; set; }

Add it to CarViewModel.cs as well:

[Display(Name = "Image ID")]
public string ImageId { get; set; }

We won’t yet see the image itself until the next post so we’ll only show the image id first. Locate ConvertToViewModel in DomainExtensions.cs and add the new property to the conversion:

CarViewModel carViewModel = new CarViewModel()
{
	Id = carDomain.Id
	, DailyRentalFee = carDomain.DailyRentalFee
	, Make = carDomain.Make
	, NumberOfDoors = carDomain.NumberOfDoors
	, ImageId = carDomain.ImageId
};

In Views/Cars/Index.cshtml we’ll extend the table to view the image IDs:

...
<th>
	@Html.DisplayNameFor(model => model.ImageId)
</th>
...

<td>
	@Html.DisplayFor(modelItem => item.ImageId)
</td>

...

Run the application to test if it’s still working. The image IDs will be empty of course in the Cars table. Click the Image link to see if it leads us to the empty Image view.

In CarsController add the following private method:

private void AttachImageToCar(HttpPostedFileBase file, Car car)
{
	ObjectId imageId = ObjectId.GenerateNewId();
	car.ImageId = imageId.ToString();
	CarRentalContext.Cars.Save(car);
	MongoGridFSCreateOptions createOptions = new MongoGridFSCreateOptions()
	{
		Id = imageId
		, ContentType = file.ContentType
	};
	CarRentalContext.CarRentalDatabase.GridFS.Upload(file.InputStream, file.FileName, createOptions);
}

We construct a new object id and set it as the ImageId property of the Car object. We then call the Save function to update the Car in the database. Then we use an overload of the Upload function to upload the file to GridFS. Note how we got hold of the default GridFS instance through the GridFS property. As we specify the ID and the content type ourselves we can use the MongoGridFSCreateOptions to convey the values. Call this function from the following POST method in CarsController:

[HttpPost]
public ActionResult Image(string id, HttpPostedFileBase file)
{
	Car car = CarRentalContext.Cars.FindOneById(new ObjectId(id));
	AttachImageToCar(file, car);
	return RedirectToAction("Index");
}

We can now fill in Image.cshtml:

@model CarRentalWeb.ViewModels.CarViewModel

@{
    ViewBag.Title = "Image";
}

<h2>Car image</h2>

@using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
	@Html.AntiForgeryToken()

	<div>
        
		<div>
			<label>Make: </label>
			<div>
				@Model.Make
			</div>
		</div>
		
		<div>
			<label>Image: </label>
			<div>
				<input type="file" id="file" name="file"/>
			</div>
		</div>

		<div>
			<div>
				<input type="submit" value="Save"/>
			</div>
		</div>
	</div>
}

<div>
	@Html.ActionLink("Back to Cars", "Index")
</div>

Specifying multipart form data in “enctype = multipart/form-data” will enable us to post the entire form data along with the posted file. Run the application, navigate to /cars, link on the Image link on one of the cars and post an image file. We haven’t included any user input checks but in a real application we would validate the user input. For now just make sure that you post an image file: jpg, jpeg, png, gif etc. We’ll show the file in the next post. Don’t worry if you don’t have images showing cars, that’s not the point.

If everything goes well then the Cars table should show the image IDs:

Image IDs shown in the Cars table

In the next post, which will be the last in this series on MongoDb, we’ll show and update the image.

View the posts related to data storage here.

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a comment

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.