A Photo Stream For the Cloud using ASP.NET MVC and Azure Blob Storage

Posted by: Suprotim Agarwal , on 5/24/2013, in Category Microsoft Azure
Views: 85309
Abstract: In this article, we will use a bunch of familiar technologies to build a nice little image hosting application. We will build an ASP.NET MVC app that uses Azure Blob Storage for hosting the images. The MVC app can be easily hosted on Azure Website (not demoed here). Idea is to see multiple technologies that we have seen working in samples work together in a slightly bigger scenario.

Today we will see how a more involved application like a picture viewer can potentially use Azure Blob Storage. For the front end, we will build an ASP.NET MVC application and for the UI we will use CSS and KnockOut JS to spin up a decent looking UI. We will also use Ninject as our DI Container.

The functional Requirement

We are building an image service that allows users the ability to Upload photos to their Accounts and label them. So apart from having registered users, we will have multiple images stored per user.

When the user logs in, they should be presented with a nice ‘wall’ of their uploaded images and there should be an un-obtrusive method for uploading more images if desired and these should get added to the photo wall.

 

Application Design

We will create a standard three tiered structure using the Repository pattern to store the data in databases. The Repositories will be injected into the Controllers using Ninject. Finally in the View layer, we will use a mix of standard scaffolding and custom UI to load and view images in the system.

Image files will be uploaded to Windows Azure Blob storage while the metadata will be stored in the Database.

Project Setup

Getting Started – The Application Structure

Step 1: We start off with an MVC 4 application Internet Application Template, this pulls in the basic forms authentication implementation.

Step 2: We install MvcScaffolding using the Nuget Package Manager. This will help us scaffold the repositories for our Entities.

Step 3: We install Ninject as follows

PM> install-package ninject

PM> install-package ninject.mvc3

Step 4: (Optional) Update jQuery to 1.9.1, jQuery UI, jQuery Validator to their respective latest versions using Nuget Package Management Console.

Step 5: Add the following setting to the web.config for Azure Blob Storage

<add key="StorageConnectionString" value="UseDevelopmentStorage=true" />

Implementation

Step 1: Add the class Photo in the Models folder with the following properties

public class Photo
{
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Notes { get; set; }
public string UserId { get; set; }
}

Step 2: Build the application. Now right click on the Controllers folder and select Add Controller. In the Add Controller wizard, select the options as follows. Note the MvcScaffolding Template that we are using to generate the Repositories for us.

photo-controller-mvcscaffolding

This will generate the Controller, the Views, the Repository interface and the Repository implementation. We will move the PhotoRepository to a separate Repository folder.

Integrating Ninject

To get Ninject to create the Repository dependencies for us, we need to make a single line of change. In the App_Start\NinjectWebCommon.cs class, add the mapping function in the RegisterServices method as follows

/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IPhotoRepository>().To<PhotoRepository>();
}

This ensures whenever MVC is looking for IPhotoRepository Ninject is able to supply an instance of it.

Adding Upload Controller and Views

Once the repository is configured, the data can be saved to database. Let’s take care of file upload. We will use the same implementation of uploading multiple files using the chunked upload technique that we saw earlier. We will look at the key differences here.

The View

Instead of just the progress bars, we’ll have a place holder for the image as well which will show the image once uploaded. Additionally we’ll have an editable input box that has the Notes property of the Photo. By default Name is set to the Notes field as well.

file-uploading-azure-blob

The View markup is as follows, we can see the additional fields that we have added to the view model i.e. uploadedUrl, name and the Save function that is bound to the click of the Save method.

<div class="content-wrapper">
<h2>Upload files to your Library</h2>
<div class="content-wrapper">
  <input id="selectImages" type="file" multiple="multiple" value="Select Files" />
  <input id="uploadImages" type="button" value="Upload" />
</div>
<div data-bind="foreach: uploaderCollection" style="width: 100%; height: 100%">
  <div data-bind="with: $data" style="border: 1px solid gray; float: left; width: 400px; height: 300px; text-align: center; margin-right: auto; margin-left: auto;">
   <div style="width: 300px; height: 200px; margin-right: auto; margin-left: auto;">
    <img data-bind="attr: { src: uploadedUrl }" alt="Uploading" style="width: 100%; height: 100%" />
   </div>
   <div class="progressBar" style="width: 300px; height: 10px; background-color: grey;
    margin-right: auto; margin-left: auto;"
    data-bind="attr: { id: 'progressBar' + fileIndex }">
   </div>
   <label style="font-size: x-small" data-bind="attr: { id: 'statusMessage' + fileIndex
    }"></label>
  
<input type="text" data-bind="value: name" />
   <input type="button" data-bind="click: save" value="Save" />
   <a data-bind="attr: { src: uploadedUrl }, text: uploadedUrl"></a>

  </div>
</div>
</div>
@section Scripts{
    <script src="~/Scripts/knockout-2.2.0.debug.js"></script>
    <script src="~/Scripts/apm-uploader.js"></script>
}

The View Model

The view model has changes required to support the updated view. These are highlighted below, I have removed most of the unchanged parts for brevity:

/// <reference path="jquery-1.8.2.js" />
/// <reference path="_references.js" />
$(document).ready(function ()
{

});

var beginUpload = function ()
{

}

var uploaders = {

}

var chunkedFileUploader =
{
maxRetries: 3,
blockLength: 1048576,
numberOfBlocks: 1,
currentChunk: 1,
retryAfterSeconds: 3,
fileToBeUploaded: null,
size: 0,
fileIndex: 0,
name: "",
init: function (file, index)
{
  this.fileToBeUploaded = file;
  this.size = file.size;
  this.name = file.name;
  this.fileIndex = index;
 
this.uploadedUrl = ko.observable();
  this.id = -1;

},
uploadMetaData: function ()
{
  this.numberOfBlocks = Math.ceil(this.size / this.blockLength);
  this.currentChunk = 1;
  $.ajax({
   type: "POST",
   async: true,
    url: "/Upload/SetMetadata?blocksCount=" + this.numberOfBlocks
     + "&fileName=" + this.name
     + "&fileSize=" + this.size
     + "&fileIndex=" + this.fileIndex,
    }).done(function (state)
    {
     if (state.success == true)
     {
      var cufl = uploaders.uploaderCollection()[state.index]
      cufl.displayStatusMessage(cufl, "Starting Upload");
      cufl.sendFile(cufl);
     }
    }).fail(function ()
    {
     this.displayStatusMessage("Failed to send MetaData");
    });
   },
 
save: function(uploader)
   {
    $.ajax({
     url: "/Photos/Edit",
     type: "POST",
     async: true,
     contentType: "application/json",
     data: JSON.stringify( {
      Id: uploader.id,
      Url: uploader.uploadedUrl(),
      Notes: uploader.name
     })
    });
   },

   sendFile: function (uploader)
   {
    …
    jqxhr = $.ajax({
     async: true,
     url: ('/Upload/UploadChunk?id=' + uploader.currentChunk + "&fileIndex=" +
           uploader.fileIndex),
     data: fileChunk,
     cache: false,
     contentType: false,
     processData: false,
     type: 'POST'
    }).fail(function (request, error)
    {
     …
    }).done(function (state)
    {
    if (state.error || state.isLastBlock)
    {
      
cful.displayStatusMessage(cful, state.message);
       cful.updateProgress(cful);
       cful.uploadedUrl(state.url);
       cful.id = state.id;
       return;
    }
    ++cful.currentChunk;
    start = (cful.currentChunk - 1) * cful.blockLength;
    end = Math.min(cful.currentChunk * cful.blockLength, cful.fileToBeUploaded.size);
    retryCount = 0;
    cful.updateProgress(cful);
    if (cful.currentChunk <= cful.numberOfBlocks)
    {
     sendNextChunk();
    }
   });
  }
  sendNextChunk();
},
displayStatusMessage: function (uploader, message)
{
  $("#statusMessage" + uploader.fileIndex).text(message);
},
updateProgress: function (uploader)
{
  var progress = uploader.currentChunk / uploader.numberOfBlocks * 100;
  if (progress <= 100)
    {
     $("#progressBar" + uploader.fileIndex).progressbar("option", "value", parseInt(progress));
     uploader.displayStatusMessage(uploader, "Uploaded " + progress + "%");
    }
   }
}

The Controller

In the upload Controller we made a critical change on naming of our blob. Earlier we used to dump everything in a hard-coded blob, but now since the photostreams should not be mixed up, we will separate our blobs per user. We change changed the blob creation code as highlighted below

[HttpPost]
[Authorize]
public ActionResult SetMetadata(int blocksCount, string fileName, long fileSize, int fileIndex)
{
var container = CloudStorageAccount.Parse(
ConfigurationManager.AppSettings["StorageConnectionString"]).CreateCloudBlobClient()
        .GetContainerReference(Request.RequestContext.HttpContext.User.Identity.Name);
container.CreateIfNotExists();
var fileToUpload = new CloudFile()
{
  BlockCount = blocksCount,
  FileName = fileName,
  Size = fileSize,
  BlockBlob = container.GetBlockBlobReference(fileName),
  StartTime = DateTime.Now,
  IsUploadCompleted = false,
  UploadStatusMessage = string.Empty,
  FileKey = "CurrentFile" + fileIndex.ToString(),
  FileIndex = fileIndex
};
Session.Add(fileToUpload.FileKey, fileToUpload);
return Json(new { success = true, index = fileIndex });
}


We have also added properties to the object that is returned once the entire file is upload. Also at this point, we have saved the image data to the repository. Both these changes are in the CommitAllChunks method and is highlighted below. Also note the new Constructor in which an instance of the PhotosRepository is injected

private ActionResult CommitAllChunks(CloudFile model)
{
 
IPhotoRepository _photosRepository;
public UploadController(IPhotoRepository photos)
{
  _photosRepository = photos;
}

  …
  _photosRepository.InsertOrUpdate(new Photo
  {
   Name = model.FileName,
   Notes = model.FileName,
   Url = model.BlockBlob.Uri.ToString(),
   UserId = Request.RequestContext.HttpContext.User.Identity.Name
  });
  _photosRepository.Save();
}
catch (StorageException e)
{
  model.UploadStatusMessage = "Failed to Upload file. Exception - " + e.Message;
  errorInOperation = true;
}
finally
{
  Session.Remove(model.FileKey);
}
return Json(new
{
  error = errorInOperation,
  isLastBlock = model.IsUploadCompleted,
 
message = model.UploadStatusMessage,
  index = model.FileIndex,
 
url = model.BlockBlob.Uri
});
}

Updating the Repository

In the Photos repository, we update the default implementation of InsertOrUpdate implementation. This is because the default implementation bases the Insert or Update based on Id value = 0. However in our case, if Id value is 0 but the name is same, the file will get overwritten in blob storage. So we shouldn’t be inserting a new database entry in that case. To do this we update the InsertOfUpdate method as follows

public void InsertOrUpdate(Photo photo)
{
using (AzurePhotoManagerContext context = new AzurePhotoManagerContext())
{
  Photo current = null;
  if (photo.Id == default(int))
  {
   // New entity
   current = context.Photos.FirstOrDefault<Photo>(p => p.Name == photo.Name);
   if (current == null)
   {
    context.Photos.Add(photo);
   }
   else
   {
    photo.Id = current.Id;
    context.Entry(photo).State = EntityState.Modified;
   }
  }
  else
  {
   // Existing entity
   context.Entry(photo).State = EntityState.Modified;
  }
  context.SaveChanges();
}
}

Notice another change, we are using a local EF Context. This is required because Repository lifecycle is managed by Ninject and Ninject by default does not create a new instance on every request. Though it can be setup to do so quite easily.

On similar lines we update the Delete method

public void Delete(int id)
{
using (AzurePhotoManagerContext context = new AzurePhotoManagerContext())
{
  var photo = context.Photos.Find(id);
  context.Photos.Remove(photo);
  context.SaveChanges();
}
}

This makes the Save method redundant, we can remove it we want to. We leave the shared context for doing selects and searches.

The Home Page and the Home Controller

Now that the Upload Controller is good to go, we can setup the Index page and get data to be displayed on the Home Page. Mind you authentication is required in every step and a user can see only their Images.

Markup Changes

The Home\Index.cshtml that comes up by default is updated to render images only if the user is authenticated. So, when the User is not Authenticated, we just show them a message requesting them to Log on.

@{
    ViewBag.Title = "Azure Photo Stream";
}
@if (!Request.IsAuthenticated)
{
@section featured {
  <section class="featured">
   <div class="content-wrapper">
    <hgroup class="title">
     <h1>Log in to view your photo stream</h1>
    </hgroup>
    <p>
     For more Azure Tutorials Visit
     <a href="
https://www.dotnetcurry.com/BrowseArticles.aspx?CatID=73" title="DotNetCurry - Windows Azure Tutorials">www.dotnetcurry.com</a>.                   
    </p>
   </div>
  </section>
}
}

Once logged on, we have setup the UI to databind to the list of photos returned by the PhotosController’s Home method

else
{
<h1>Your Photo Stream</h1>
<div data-bind="foreach: photos" style="width: 100%; height: 100%; margin-right: auto; margin-left: auto;">
<div data-bind="with: $data" style="border: 1px solid gray; float: left; width: 350px; height: 300px; padding: 5px;
text-align: center; margin-right: auto; margin-left: auto;">
  <div style="width: 300px; height: 200px; margin-right: auto; margin-left: auto;">
   <a data-bind="attr: { href: Url }">
    <img data-bind="attr: { src: Url }" alt="Uploading" style="width: auto; height: 100%" />
   </a>
  </div>
  <div>
   <label data-bind="text: Name" />
  </div>
  <div>
   <label data-bind="text: Notes" />
  </div>
</div>
</div>
}

@section Scripts{
<script src="~/Scripts/knockout-2.2.0.js"></script>
<script src="~/Scripts/apm-home.js"></script>
}

The Photos Controller Home Method

We add a new Home action method that returns a JsonResult to our home page with the top 20 photos for the user. For brevity, we won’t implement pagination today.

The Home action method is as follows

public JsonResult Home()
{
    var userId = Request.RequestContext.HttpContext.User.Identity.Name;
    return Json(photoRepository.All.Where<Photo>(l=>l.UserId==userId).Take<Photo>(20),
     JsonRequestBehavior.AllowGet);
}

With that we wrap up all the new code and changes required. Time for a test run.

Testing it all out

First thing is to start the Azure Storage Emulator. From the Azure Command Prompt you can type

csrun /devstore

This starts the Azure storage emulator. All set, let’s run the App. Also using the Visual Studio DB Explorer, ensure that you have navigated

Step 1: From home page click on Register to create a new account and login

empty-home-page

Step 2: Click on upload to navigate to the Upload page, select a set of files and upload. We already saw the UI for the Upload page above.

Step 3: Now click on ‘Azure Photo Studio’ to go to your home page

uploaded-home-page

Editing the Name and Labels

We can use the Scaffolder generated pages to update Name and Labels. Click on ‘Library’ on the top right corner page to go to your image library. Click on Edit and edit the Notes for one of the images

edit-notes

Save and navigate back to your home page. As seen below, we have the update note coming up in the home page

updated-notes-azure-blob

From the Library page you can manage your images and delete them if required.

Our Photo Management portal is ready! Of course it is rather crude at the moment and can have lots of enhancements, but overall we have moved one step closer towards building complete applications in ASP.NET MVC using Azure Blob storage.

Conclusion

Using Azure blob storage in an ASP.NET MVC app is rather seamless and once we plan our application out, it’s pretty easy to build upon. Today we used some of our past experience to quickly put together a functional Image Library service, which albeit can do with a lot more features in the future.

Download the entire source code of this article (Github)

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of DotNetCurry, DNC Magazine for Developers, SQLServerCurry and DevCurry. He has also authored a couple of books 51 Recipes using jQuery with ASP.NET Controls and The Absolutely Awesome jQuery CookBook.

Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.

Get in touch with him on Twitter @suprotimagarwal or at LinkedIn



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by anil on Monday, August 12, 2013 4:33 AM
Title:Upload Images to Azure storage Account Inserted Information Store to Database using MVC

Hi,

TABLE: 

ID   Name  ImageURL

1     aaa      http://example/Blob/Images/1.jpg

At the time of inserting the data insert into sqlserver database along with Image Url like above example,The Image stored to Azure Storage account,

At the time of Uploading image automatically store data in database above formate(Example I am giving)Images are store in Azure storage account using MVC

In the same way Update,Delete,Display also

Can you give me this example code or give me any sample example for this

help me
Comment posted by Malcolm on Monday, September 23, 2013 2:58 AM
I am having problems getting this to display images. I have uploaded into blob image in in the Blob but does not display on the site. Any Ideas?
Comment posted by appdynamics vs new relic on Saturday, January 31, 2015 6:43 AM
Nice read ! You do not need to hard code log statements. At runtime, you can register a class of interest and the system will log all important information about the class. When you are done, you can de-register the class and the class will not generate any log information.
http://blog.takipi.com/appdynamics-vs-new-relic-which-tool-is-right-for-you-the-complete-guide/