This post’s main objective was originally about completing the initial skeleton of uploading an image from a web page and generating a thumbnail from an Azure Web Job using Azure Blob Storage and Azure Queues, but it turned into a pretty large refactoring in anticipation of having something a bit more realistic to eventually post to GitHub. So, I’ll devote the first part of the post to a brief review of the most significant changes and then introduce the Azure Web Job into the mix and finally, I’ll retrieve the message from the Azure Queue and show its name in the console. I’ll then devote Azure Bit #5 to processing the original image and generating a thumbnail to complete the initial skeleton of our Image Manipulator application.
Changing the Serialization Strategy
I’m not sure why I originally chose to go with serializing my UploadedImage to a ByteArray before inserting it into the Azure Queue, but I’ve now simplified things a bit and switched over to serializing my UploadedImage as JSON. This allows me to drop the ByteArray extension methods that I previously added to the project and it buys me some nice auto-hydration of my UploadedImage later when my processing function is called in the Azure Web Job. Revisiting the AddMessageToQueueAsync method in my QueueService, we now convert the UploadedImage to JSON instead of a ByteArray. Note that you’ll need to include the Newtonsoft.JSON Nuget package for the JSON serialization if it’s not already present in your web project.
- public async Task AddMessageToQueueAsync(string messageId, T messageObject)
- {
- var queue = GetQueue();
- // Convert to JSON
- var jsonMessage = JsonConvert.SerializeObject(messageObject);
- // Create the actual queue message
- CloudQueueMessage message = new CloudQueueMessage(jsonMessage);
- // Add the message to the queue
- await queue.AddMessageAsync(message);
- }
In addition, I can revert the Data property in my UploadedImage class to an Auto Property since I no longer need to bother with the [NonSerialized] attribute. I can also remove the [Serialized] attributes from my models. Instead, I will place a [JsonIgnore] attribute on my Data property and it will be skipped in the JSON serialization process. I’ve also removed the previous hard-coded inclusion of one thumbnail from my constructor for UploadedImage. My models now look like this:
- public class UploadedImage
- {
- public UploadedImage()
- {
- Thumbnails = new List<Thumbnail>();
- }
- public string Name { get; set; }
- public string ContentType { get; set; }
- [JsonIgnore]
- public byte[] Data { get; set; }
- public string Url { get; set; }
- public List<Thumbnail> Thumbnails { get; set; }
- }
- public class Thumbnail
- {
- public int Width { get; set; }
- public int Height { get; set; }
- public string Url { get; set; }
- public string Name { get; set; }
- }
Adding Dependency Injection
I had planned to get through this Azure Bits series without Dependency Injection, but I decided I missed it and didn’t want to promote the practice of new-ing up the dependencies directly. So, I’ve added Ninject to the solution and replaced all direct instantiation of my dependencies. It’s super easy to add Ninject via Nuget and there’s even a Ninject.MVC5 package available to get you started on wiring the dependencies. This package will insert a configuration file for Ninject in your App_Start directory. The important changes I needed to make to the default Ninject configuration file were to the RegisterServices method, where the actual wiring of the dependencies takes place. You can see that I am also injecting the appSettings and connectionString into my dependencies allowing me to mock these more easily for unit tests.
- private static void RegisterServices(IKernel kernel)
- {
- kernel.Bind<IImageService>().To<ImageService>()
- .WithConstructorArgument(“containerName”, ConfigurationManager.AppSettings["ImagesContainer"])
- .WithConstructorArgument(“imageRootPath”, ConfigurationManager.AppSettings["ImageRootPath"])
- .WithConstructorArgument(“connectionString”, ConfigurationManager.ConnectionStrings["BlobStorageConnectionString"].ConnectionString);
- kernel.Bind<IQueueService<UploadedImage>>().To<QueueService<UploadedImage>>()
- .WithConstructorArgument(“queueName”, ConfigurationManager.AppSettings["ImagesQueue"])
- .WithConstructorArgument(“connectionString”, ConfigurationManager.ConnectionStrings["BlobStorageConnectionString"].ConnectionString);
- }
With Ninject now in place, we add a constructor to HomeController so we can inject the dependencies via Constructor Injection. Also, I moved the creation of the default thumbnail to the controller for now. Other than these changes, the HomeController remains unchanged.
- public class HomeController : Controller
- {
- private readonly IImageService _imageService;
- private readonly IQueueService<UploadedImage> _queueService;
- public HomeController(IImageService imageService, IQueueService<UploadedImage> queueService)
- {
- _imageService = imageService;
- _queueService = queueService;
- }
- public ActionResult Index()
- {
- return View(new UploadedImage());
- }
- [HttpPost]
- public async Task<ActionResult> Upload(FormCollection formCollection)
- {
- var model = new UploadedImage();
- if (Request != null)
- {
- HttpPostedFileBase file = Request.Files["uploadedFile"];
- model = await _imageService.CreateUploadedImage(file);
- // hard-coded adding one thumbnail for now
- model.Thumbnails.Add(new Thumbnail { Width = 200, Height = 300 });
- await _imageService.AddImageToBlobStorageAsync(model);
- await _queueService.AddMessageToQueueAsync(model.Name, model);
- }
- return View(“Index”, model);
- }
- }
New Format For The Message in Azure Queue
After making these changes, you should be able to run the application and upload an image to Azure Blog Storage and the new JSON version of your UploadedImage object should be sitting in the Azure Queue as a message waiting to be picked up by your Azure Web Job. Taking a peek at the images message queue confirms that this is indeed the case:
Creating the Azure Web Job
After that quick refactoring detour, we are now ready to get on with the business of creating our Azure Web Job which will monitor our Azure Queue for UploadedImage messages and will automatically kick off the processing of thumbnails any time a new UploadedImage is added to the Queue. There is a nice Azure Web Job project type available in Visual Studio. Simply right-click the solution and choose Add a New Project and look under the Cloud folder for the Azure Web Job project.
This will create a console application with a few hooks in place for the Azure Web Job. You’ll first see the standard Program.cs. Here there will be some default code in the Main method that will create the JobHost and then run it. This default code assumes that you have connection strings in place either in Azure or in your app.config that are named explicitly as AzureWebJobsDashboard and AzureWebJobsStorage. If it can’t find these connection strings based on the default implementation of the Main method, you’ll get errors. There is an overloaded constructor for JobHost that takes an instance of JobHostConfiguration that allows more control over the connection strings and other configuration information for this Web Job. I prefer to explicitly set the connection strings and then I can name them whatever I want. So, my Main method looks like this:
- public static void Main()
- {
- var connectionString = ConfigurationManager.ConnectionStrings["BlobStorageConnectionString"].ConnectionString;
- var config = new JobHostConfiguration
- {
- DashboardConnectionString = connectionString,
- StorageConnectionString = connectionString
- };
- var host = new JobHost(config);
- host.RunAndBlock();
- }
The other important file that gets created by the Azure Web Jobs template is the functions.cs file. This is the file that contains the function (or functions) that listen to specific Azure queues and are called when new messages are added. There is only function added in the default implementation. I am going to replace the parameters with ones more appropriate for our task at hand. You’ll notice that the first parameter in my implementation is an UploadedImage and you’ll note that the parameter is marked with a [QueueTrigger(“images”)] attribute. This is what tells Azure to call this method when the Queue named “images” enqueues a new message.
Moving to the second parameter, originalImageStream… this parameter is marked with the [Blob(“images/{Name}”)] attribute. If you recall, the UploadedImage class has a Name property that contains the name of the original image that we inserted into Azure Blob Storage. When ProcessQueueMessageAsync is called, Azure hydrates the first parameter, UploadedImage, from the JSON message we inserted into the Queue and then it looks at the Name property (or whatever property we specify in the braces of our Blob attribute). With this Blob attribute, we are telling Azure to go to my container named “images” and get the image that matches the uploadedImage.Name property and then deliver this to me as a stream in my processing method. This is very helpful as the first thing you’d want to do without this automatic delivery of the source stream is go to Azure Blob Storage and fetch it. This is what my ProcessQueueMessageAsync method looks like now.
- public class Functions
- {
- // This function will get triggered/executed when a new message is written
- // to an Azure Queue called “images”.
- // Parameters:
- // uploadedImage – automatically hydrated from Azure Queue where we placed the message as JSON object
- // originalImageStream – takes the Name property from uploadedImage and grabs image from blob storage
- public async static void ProcessQueueMessageAsync(
- [QueueTrigger("images")] UploadedImage uploadedImage,
- [Blob("images/{Name}", FileAccess.Read)] Stream originalImageStream)
- {
- Console.WriteLine(“Processing image: “ + uploadedImage.Name);
- }
- }
Testing the Azure Web Job
If you right-click your Web Job project and choose Publish as Azure Web Job, your Web Job should get published to your Azure Web Site. You can double-check that in the Azure Portal by going to Web Apps and selecting Web Jobs.
Ideally, you would want your Web Job running continuously so that it would be called automatically when a new image is added, but depending on the type of Azure account that you have, you may not have the ability to run your Web Job continuously. However, you can still test it by right-clicking the project in the Solution Explorer and choosing Debug > Start New Instance. Assuming you have a message in your queue, you should see something similar to the below and you should see that the message is now removed from your Azure Queue.
Pausing for Now
Now that we are successfully fetching our UploadedImage message and our original image, we just need to process the received image file into the thumbnail image(s) specified in our UploadedImage.Thumbnails collection. In the next Azure Bit, I’ll introduce an ImageProcessor and add the functionality to generate a thumbnail and save it to Azure Blob Storage. If you missed the other posts in this series, you can start with Azure Bits #1 – Up and Running.
Originally published: 2015/07/24