Track Number of File Downloads in ASP.NET MVC
A common requirement in content management systems, forums and portals is to
track the number of times a file has been downloaded. Such a tracking gives you
an idea as to how many users downloaded a file and thus can also hint at the
popularity / usefulness of the content. One can always put a hyperlink directly
pointing to the file that is to be downloaded. However, such direct links don't
reveal any information about the number of times a file was downloaded. Luckily,
with a little bit of coding you can track file downloads in ASP.NET MVC. Let's
see how.
Database table for storing file details
In order to track file downloads firstly you should avoid direct links to the
file under consideration. You can generate hyperlinks that point to an action
method which in turn returns the file. This indirect way of returning files
allows you to track the number of downloads (and also users who downloaded it,
in case membership is enabled). Since hyperlinks are no longer pointing to files
themselves, you need a database table that stores a list of file IDs and their
actual path. Consider the following database table that shows how this can be
done:

The Files table has five columns - FileID, FileDisplayName, FilePath,
FileType and DownloadCount. The FileID is an identity column. The
FileDisplayName column contains a friendly name of a file that is displayed to
the user. The FilePath column contains the ~ qualified virtual path of the file.
This path is never shown to the end user. It is used by the action method
internally (I will discuss this shortly). The FileType column holds the MIME
content type for a file. This value is used while downloading the file. Finally,
the DownloadCount column stores the number of times a file was downloaded.
Displaying a list of files
Now, let's display a list of files that can be downloaded to the end user. To
do this you need to fetch data from the Files table discussed earlier and supply
it as a model to the Index view. The Index() action looks like this:
public ActionResult Index()
{
using (FilesDbEntities db = new FilesDbEntities())
{
return View(db.Files.ToList());
}
}
The Index view then iterates through the List of files and displays them in a
table:
@model List<CSVDownloadDemo.Models.File>
...
<h1>List of Files</h1>
<table border="1" cellpadding="10">
<tr>
<th>File ID</th>
<th>File Name</th>
<th>File Type</th>
<th>Downloads</th>
<th>Action</th>
</tr>
@foreach(var item in Model)
{
<tr>
<td>@item.FileID</td>
<td>@item.FileDisplayName</td>
<td>@item.FileType</td>
<td>@item.DownloadCount</td>
<td>@Html.ActionLink("Download", "DownloadFile",
new { id=item.FileID })</td>
</tr>
}
</table>
...
Notice the code marked in bold letters. The ActionLink() call points to
DownloadFile() action method and passes FileID as a route parameter to this
method. You will write this method shortly.
The following figure shows how the table looks like in the browser:

Sending files from the server to the client
Now comes the important part - the DownloadFile() action method. This method
receives FileID as a parameter, increments the download count for that file and
sends the file to the client for download. The DownloadFile() is shown below:
public FilePathResult DownloadFile(int id)
{
using (FilesDbEntities db = new FilesDbEntities())
{
FileDownloadDemo.Models.File file = db.Files.Find(id);
file.DownloadCount++;
db.SaveChanges();
string downloadName=file.FileDisplayName +
VirtualPathUtility.GetExtension(file.FilePath);
return File(file.FilePath, file.FileType, downloadName);
}
}
The DownloadFile() method increments the DownloadCount column value and calls
SaveChanges() to save the changes back to the database. It then computes a file
name that is displayed in the "Save As" dialog. Notice how VirtualPathUtility
class is used while forming the file name. This file name can be anything of
your choice. I am using the name same as the FileDisplayName value. The
DownloadFile() method returns a FilePathResult (instead of usual ActionResult).
To wrap a file in FilePathResult and send it to the client you use File()
method. The File() method takes three parameters - virtual path of the file,
MIME type of the file and name to be displayed in the "Save As" dialog.
If you run the application and click on the Download link you should see
something like this:

Notice that the "Save As" dialog is showing the file name as per the third
parameter supplied to the File() method.
Other variations of File() method
There are a couple of more variations of the File() method. These variations
basically allow you to alter the source of the file content. In the preceding
example the source of file content was a physical disk file. What if a file is
stored directly in the database as a binary (BLOB) data? Or what if a file is
being generated programmatically on-the-fly based on some logic? The other two
variations of File() cover these possibilities.
Consider the following code that shows a DownloadBytes() action method.
public FileContentResult DownloadBytes(int id)
{
using (FilesDbEntities db = new FilesDbEntities())
{
...
string path=VirtualPathUtility.ToAbsolute(file.FilePath);
byte[] bytes = System.IO.File.ReadAllBytes(path);
return File(bytes, file.FileType, downloadName);
}
}
Here the file content is generated as a byte array (a more realistic case
would be when a file is stored in the database). The byte array is then passed
to the first parameter of File() method. Note that the return type of
DownloadBytes() is FileContentResult.
Now consider one more variation.
public FileStreamResult DownloadStream(int id)
{
using (FilesDbEntities db = new FilesDbEntities())
{
...
string path = VirtualPathUtility.ToAbsolute(file.FilePath);
System.IO.Stream stream = System.IO.File.OpenRead(path);
return File(stream, file.FileType, downloadName);
}
}
Here the file is opened into a Stream using OpenRead() method (a more
realistic case is when a file is generated on-the-fly on the server). The Stream
is then passed to the first parameter of the File() method. Note that the return
type of DownloadStream() is FileStreamResult.
That's it for now! Keep coding!!