Thursday, October 1, 2009

More on nopCommerce for Azure

I penned my previous post on nopCommerce for Azure around 1 AM one morning, having just spent the last few hours playing with the code and deploying to Azure.  In my sleepy stupor, I left off a few details about how it’s implemented and the changes required.  So I thought I’d post a few more of the gory details for those interested.  A zipped version of the nopCommerce 1.30 code, including the slightly altered Azure projects, is available here.  You’ll need Windows Azure Tools and Windows Azure SDK installed on your PC, a Windows Azure CTP account, and a SQL Azure account (register and download here).  To use, simply follow the steps below to set up your database, and then change ConnectionStrings.config to use the connection string to your database.  You should then be able to run locally (choose AzureNopCommerceStore as the startup project) against the SQL Azure database.  Once you’re happy with that, rt click AzureNopCommerceStore  and choose publish to publish the project to Azure.

I used the SQL Azure Migration Wizard, but had to address some minor discrepencies between SQL 2008 and SQL Azure manually.  One procedure, [dbo].[Nop_Maintenance_ReindexTables], contains code that cannot be modified to run on SQL Azure, so that functionality won’t work.  A fixed up script is in the App_Data folder of the AzureNopCommerce_WebRole project.  Simply run that script against your SQL Azure database and then copy data from a live database, or run the sample data script also included in that folder.  Tip: Sql Management Studio will give you some errors, but if you follow these steps exactly, you can get a query window connected to SQL Azure to run these scripts.

The code for the most part worked as-is, except that any part that tries to write to disk will throw a FileIOPermissions exception.  The main place I found this was happening was the image generation code in GetPictureUrl method inside PictureManager.  This I changed to ‘hard code’ a path to a page that will be responsible for dynamically serving up the image, GenPicture.aspx (see the last line):

/// <summary>
/// Get a picture URL
/// </summary>
/// <param name="picture">Picture instance</param>
/// <param name="TargetSize">The target picture size (longest side)</param>
/// <param name="showDefaultPicture">A value indicating whether the default picture is shown</param>
/// <returns></returns>
public static string GetPictureUrl(Picture picture, int TargetSize, bool showDefaultPicture)
{
  string url = string.Empty;
  if (picture == null)
  {
      if (showDefaultPicture)
          url = GetDefaultPictureUrl(TargetSize);
      return url;
  }
 
  string[] parts = picture.Extension.Split('/');
  string lastPart = parts[parts.Length - 1];
  switch (lastPart)
  {
      case "pjpeg":
          lastPart = "jpg";
          break;
      case "x-png":
          lastPart = "png";
          break;
      case "x-icon":
          lastPart = "ico";
          break;
  }
 
  string localFilename = string.Empty;
  if (picture.IsNew)
  {
      string filter = string.Format("{0}*.*", picture.PictureID.ToString("0000000"));
      string[] currentFiles = System.IO.Directory.GetFiles(PictureManager.LocalThumbImagePath, filter);
      foreach (string currentFileName in currentFiles)
          File.Delete(Path.Combine(PictureManager.LocalThumbImagePath, currentFileName));
 
      picture = PictureManager.UpdatePicture(picture.PictureID, picture.PictureBinary, picture.Extension, false);
  }
 
  return CommonHelper.GetStoreLocation(false) + "images/genpicture.aspx?p=" + picture.PictureID + "&s=" + TargetSize;
}

An overload for GetDefaultPictureUrl also had to be modified to, for now, not do any resizing.

public static string GetDefaultPictureUrl(PictureTypeEnum DefaultPictureType, int TargetSize)
{
   string defaultImageName = string.Empty;
   switch (DefaultPictureType)
   {
       case PictureTypeEnum.Entity:
           defaultImageName = SettingManager.GetSettingValue("Media.DefaultImageName");
           break;
       case PictureTypeEnum.Avatar:
           defaultImageName = SettingManager.GetSettingValue("Media.Customer.DefaultAvatarImageName");
           break;
       default:
           defaultImageName = SettingManager.GetSettingValue("Media.DefaultImageName");
           break;
   }
   
 
   string relPath = CommonHelper.GetStoreLocation(false) +
           "images/" + defaultImageName;
 
   return relPath;
}

GenPicture.aspx then reuses some of PictureManager’s logic to generate the image on the fly and render it to the client.  To be clear, this of course is terribly inefficient and is just a proof-of-concept hack. If you use this in production, you’ll be laughed at and maybe worse.  The better solution would be to change PictureManager to use a provider model. A FileSystemPictureProvider could then handle pictures for traditional web apps, while an AzurePictureProvider could use Azure Blob storage.  But for demo purposes, this was the fastest route to a solution.  All markup, except the @Page declaration is removed from the aspx, and the full listing for GenPicture.aspx.cs is here:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using NopSolutions.NopCommerce.Common.Media;
using NopSolutions.NopCommerce.DataAccess.Media;
 
namespace AzureNopCommerceStore_WebRole.images
{
    public partial class GenPicture : System.Web.UI.Page
    {
        private static Picture DBMapping(DBPicture dbItem)
        {
            if (dbItem == null)
                return null;
 
            Picture item = new Picture();
            item.PictureID = dbItem.PictureID;
            item.PictureBinary = dbItem.PictureBinary;
            item.Extension = dbItem.Extension;
            item.IsNew = dbItem.IsNew;
 
            return item;
        }
        /// <summary>
        /// Gets a picture
        /// </summary>
        /// <param name="PictureID">Picture identifier</param>
        /// <returns>Picture</returns>
        public static Picture GetPictureByID(int PictureID)
        {
            DBPicture dbItem = DBPictureProvider.Provider.GetPictureByID(PictureID);
            Picture picture = DBMapping(dbItem);
            return picture;
        }
        public static Size CalculateDimensions(Size OriginalSize, int TargetSize)
        {
            Size newSize = new Size();
            if (OriginalSize.Height > OriginalSize.Width) // portrait 
            {
                newSize.Width = (int)(OriginalSize.Width * (float)(TargetSize / (float)OriginalSize.Height));
                newSize.Height = TargetSize;
            }
            else // landscape or square
            {
                newSize.Height = (int)(OriginalSize.Height * (float)(TargetSize / (float)OriginalSize.Width));
                newSize.Width = TargetSize;
            }
            return newSize;
        }
        protected void Page_Load(object sender, EventArgs e)
        {
            if (String.IsNullOrEmpty(Request["s"]) || String.IsNullOrEmpty("p")) return;
            var TargetSize = int.Parse(Request["s"]);
            var pictureId = int.Parse(Request["p"]);
            var picture = GetPictureByID(pictureId);            
 
            if (TargetSize == 0)
            {
                using (MemoryStream stream = new MemoryStream(picture.PictureBinary))
                {
                   // Response.ContentType = picture.Extension;
                    stream.WriteTo(Response.OutputStream);
                }
            }
            else
            {
                using (MemoryStream stream = new MemoryStream(picture.PictureBinary))
                {
                    Bitmap b = new Bitmap(stream);
 
                    Size newSize = CalculateDimensions(b.Size, TargetSize);
 
                    if (newSize.Width < 1)
                        newSize.Width = 1;
                    if (newSize.Height < 1)
                        newSize.Height = 1;
 
                    Bitmap newBitMap = new Bitmap(newSize.Width, newSize.Height);
                    Graphics g = Graphics.FromImage(newBitMap);
                    g.DrawImage(b, 0, 0, newSize.Width, newSize.Height);                    
                    newBitMap.Save(Response.OutputStream, ImageFormat.Jpeg);
                    newBitMap.Dispose();
                    b.Dispose();
                }
            }
        }
    }
}
 
 

That is all the code changes that were required, I believe. As I said before, Azure is really not as scary as it sounds – for all the hype, you can really think of it as really cool hosting for .NET (and PHP) apps. With some notable exceptions, most code will ‘just work’ on Azure.  Because of the well thought out separation of concerns in nopCommerce, it should be possible to migrate much of the functionality to Azure Table Storage, which is cheaper than SQL Azure, and unlimited in storage size.  I may play with that in the days to come, but for now, hopefully this will wet people’s appetite for what’s possible on Azure.

1 comment:

Michael said...

Your media share file is no longer there. Can you please share again?

Thanks.