Friday, March 20, 2009

Working with Azure

I’m back in action after a long time away from my iPhone application development.  It took a very interesting weekend conference called BizCamp to get me back into doing some development again.  Thanks should also go to Stephen and Joel for getting me off the sofa, watching BSG until 1am at the weekend.

The basic idea here is to add some bug tracking functionality to my iPhone applications site.  This request came from one of my beta testers who commented that this type of functionality gives a better feeling of community.

I’m assuming that the developer reading this has all the various software downloads needed to run Azure.  I’m also assuming that you’ve gone through the pain and suffering required to obtain your invitation code to the CTP.

The Basic Design

Due to the limited data types available in Azure I’m going to keep the implementation very simple, just one table to hold all the data.

Picture 2

Each field is defined as a string however during implementation I’ll add 3 extra fields to support Azure.  ID which is a simple INT used as a Primary Key, PartitionKey a GUID used by Azure storage and RowKey which will match the Primary Key.

The User will simply fill in the error description via the website or within the iPhone application.  These records will be given a status of “Open”.

image

Later the records are reviewed within the Administration section of the website where each bug is given a status of “In Progress”, “On Hold” or “Closed”; however I’ll leave that functionality outside this posting as it’s basically using the same techniques.

Class Development

First we create 3 main classes; the first being the query definition class which encapsulates a simple list.

The context class

public class CloudContext : TableStorageDataServiceContext
{
    internal CloudContext(StorageAccountInfo accountInfo)
        : base(accountInfo)
    {  }

  // table name
internal const string BugsTableName = "BugTable";

// Query Definition
public IQueryable<UserDataModel> BugTable
      { get
           { return this.CreateQuery<BugDataModel>(BugTableName); }

     }

}

The TableStorageDataServiceContext class handles the authentication process into the Table Storage service.

The Data model

public class BugDataModel : TableStorageEntity
{
    public BugDataModel(string partitionKey, string rowKey)
        : base(partitionKey, rowKey)
    {
    }

    public BugDataModel()
        : base()
    {
        PartitionKey = Guid.NewGuid().ToString();
        RowKey = String.Empty;
    }

    public int ID
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public string Email
    {
        get;
        set;
    }

    public string ErrorDescription
    {
        get;
        set;
    }

    public string ErrorResolution
    {
        get;
        set;
    }

    public string Status
    {
        get;
        set;
    }
}

Which is a definition of the actual storage structure.

The Data Source

    public class BugDataSource
    {
        private CloudContext _ServiceContext = null;

        public BugDataSource()
        {
            // Get the settings from the Service Configuration file
            StorageAccountInfo account =
                StorageAccountInfo.GetDefaultTableStorageAccountFromConfiguration();

            // Create the service context we'll query against
            _ServiceContext = new CloudContext(account);
            _ServiceContext.RetryPolicy = RetryPolicies.RetryN(3, TimeSpan.FromSeconds(1));
        }

        public IEnumerable<BugDataModel> Select()
        {
            var results = from b in _ServiceContext.BugTable
                          select b;

            TableStorageDataServiceQuery<BugDataModel> query =
            new TableStorageDataServiceQuery<BugDataModel>(results as DataServiceQuery<BugDataModel>);
            IEnumerable<BugDataModel> queryResults = query.ExecuteAllWithRetries();
            return queryResults;
        }

        public IEnumerable<BugDataModel> SelectByID(int id)
        {
            var results = from c in _ServiceContext.BugTable
                          where c.ID == id
                          select c;
            TableStorageDataServiceQuery<BugDataModel> query =
            new TableStorageDataServiceQuery<BugDataModel>(results as DataServiceQuery<BugDataModel>);
            IEnumerable<BugDataModel> queryResults = query.ExecuteAllWithRetries();
            return queryResults;
        }

        public void Delete(BugDataModel itemToDelete)
        {
            IEnumerable<BugDataModel> bugs = this.SelectByID(itemToDelete.ID);
            foreach (BugDataModel bug in bugs)
            {
                _ServiceContext.DeleteObject(bug);
                _ServiceContext.SaveChanges();
            }
        }

        public void Update(BugDataModel itemToUpdate)
        {
            IEnumerable<BugDataModel> bugs = this.SelectByID(itemToUpdate.ID);
            foreach (BugDataModel bug in bugs)
            {
                bug.Name = itemToUpdate.Name;
                bug.Email = itemToUpdate.Email;
                bug.ErrorDescription = itemToUpdate.ErrorDescription;
                bug.ErrorResolution = itemToUpdate.ErrorResolution;
                bug.Status = itemToUpdate.Status;

                _ServiceContext.UpdateObject(bug);
                _ServiceContext.SaveChanges();
            }
        }

        public void Insert(BugDataModel newItem)
        {
            // we must overwrite the supplied ID with the MAX as Azure does not yet support Identity columns
            // with simple storeage tables.

            int Id = this.GetNextID();
            newItem.ID = Id;
            newItem.RowKey = Id.ToString();
            _ServiceContext.AddObject(CloudContext.BugTableName, newItem);
            _ServiceContext.SaveChanges();
        }

        #region  -- Private methods --
        /// <summary>
        /// Simple function to find the max results from a recordset of IDs and add one.
        /// </summary>
        /// <returns>the next id number</returns>
        private int GetNextID()
        {
            int maxID=0;
            IEnumerable<BugDataModel> categories = this.Select();
            foreach (BugDataModel Entry in categories)
                if (Entry.ID > maxID) maxID = Entry.ID;
            maxID++;
            return maxID;
        }

        #endregion

    }

The DataSource class is the real work horse of the application.  It’s the controller for getting information in and out of the storage structure.

You’ll notice that I’ve added a GetNextID private method which I’m not really happy about, but it’s the only way I could see to duplicate the functionality needed to mimic a normal database structure.

Web Interface Development

Next we create the web interface code.  In my project I separated the web code into another project and added a reference to the Model classes, but it’s not essential.

Create a new ASPX page to allow a Bug to be registered (Inserted) by the Users.

    <h2>Report your problems here</h2>
    <asp:FormView id="frmAdd" DataSourceId="bugData" DefaultMode="Insert"
            Runat="server" OnItemInserted="frmAdd_ItemInserted" >
        <InsertItemTemplate>
        <table>
        <tr>
            <td><asp:Label id="nameLabel" Text="*Name:" AssociatedControlID="nameBox" Runat="server" /></td>
            <td><asp:TextBox id="nameBox" Text='<%# Bind("Name") %>' Runat="server" />
                <asp:RequiredFieldValidator ID="reqName" ControlToValidate="nameBox" runat="server" ErrorMessage="Required"/>
            </td>
         </tr>
         <tr>
            <td><asp:Label id="emailLabel" Text="Email:" AssociatedControlID="emailBox" Runat="server" /></td>
            <td><asp:TextBox id="emailBox" Text='<%# Bind("Email") %>' Runat="server" />
            <asp:RegularExpressionValidator
                ID="RegularExpressionValidator1" runat="server"
                ControlToValidate="emailBox"
                ErrorMessage="email format"
                ValidationExpression="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"/>
            </td>
         </tr>
         <tr>
            <td><asp:Label id="errorDescriptionLabel" Text="*Description:" AssociatedControlID="errorDescriptionBox" Runat="server" /></td>
            <td><asp:TextBox id="errorDescriptionBox" Rows="10" TextMode="MultiLine" Text='<%# Bind("ErrorDescription") %>' Runat="server" />
                <asp:RequiredFieldValidator ID="reqRescription" ControlToValidate="errorDescriptionBox" runat="server" ErrorMessage="Required"/>
                <asp:TextBox id="errorResolution" Visible="false" Text='<%# Bind("ErrorResolution") %>' Runat="server" />
            </td>
         </tr>
         <tr>
            <td><asp:Label id="statusLabel" Text="Status:" AssociatedControlID="statusBox" Runat="server" /></td>
            <td><asp:TextBox id="statusBox" Value="Open" Text='<%# Bind("Status") %>' Runat="server" /></td>
         </tr>
         <tr>
            <td colspan="2"><label>* indicates a manditory field.</label>
         </tr>
         <tr align="center">
            <td colspan="2"><asp:Button id="insertButton" Text="Report" CommandName="Insert" Runat="server"/></td>
         </tr>
         </table>
        </InsertItemTemplate>
    </asp:FormView>

    <%-- Confirmation message --%>
    <asp:Panel id="frmMessage" Visible="false" runat="server">
        <p><b>Your error has been received and will be actioned as soon as possible.</b></p>
    <p><a href="Default.aspx">Return to Home</a></p>
    </asp:Panel>

    <%-- Data Sources --%>
    <asp:ObjectDataSource runat="server" ID="bugData"
        TypeName="BusinessObjects.BugDataSource" InsertMethod="Insert"
            DataObjectTypeName="BusinessObjects.BugDataModel"
            SelectMethod="Select">   
    </asp:ObjectDataSource>

As you can see I’ve created 3 main functional areas.  An ASP.FormView which has a field for each record to be inserted.  Each of these is linked to the asp:ObjectDataSource called “bugData”.  An finally an ASP:Panel which is displayed when the “insertButton” is pressed.

protected void frmAdd_ItemInserted(object sender, FormViewInsertedEventArgs e)
{
    frmAdd.Visible = false;
    frmMessage.Visible = true;
}

Above it the only code in the code behind to implement the OnPressed event.

Build everything and you should be almost ready to run a test.

Generating the local storage and running locally

Next we need to run the program locally to ensure it’s working.

First off we need to generate the database structure into the local development storage.  At this point I’m assuming that you’ve got it up an running as it worked ‘”all most” out of the box for me.  I did have to tweek the configurations as I was running under a SQL2008 database under a named instance.

I created a simple generate batch file so I could run it a few times.

echo -- Generate the Azure Storeage database
set EXE_PATH=C:\Program Files\Windows Azure SDK\v1.0\bin\
set SOURCE_PATH=C:\Development\AzureProj\Interfaces\

"%EXE_PATH%DevtableGen.exe" /forceCreate "/server:(local)" "/database:Service_Azure" "%SOURCE_PATH%bin\BusinessObjects.dll;%SOURCE_PATH%bin\ServerAzure.dll"

As you can see the DevtableGen application uses reflection to read the contents your application and will create the tables in the local storage account.

As a side note you can check the DevelopmentStorage.exe.config file and hunt out the code below to find the location of your local database.

<connectionStrings>
  <add name="DevelopmentStorageDbConnectionString"
       connectionString="Data Source=(local);Initial Catalog=DevelopmentStorageDb;Integrated Security=True"
       providerName="System.Data.SqlClient" />
</connectionStrings>

You can check that everything worked by opening SQL Management Studio.

When you run the application now you should be able to see everything in action.  I’ll admit I’ve omitted the basic instructions for creating the various projects, but Azure is not for the faint hearted so it’s fair enough to say if you need that information you really won’t be getting the most out of this posting.

Azure Deployment

Assuming you’ve got your CTP invitation you should create a Storage Project and a hosted project.

image

When you select each you’ll get the information you need to place in the configuration files.

image 

The Storage project will provide you with the Access keys and http address for accessing the hosted data. 

image

Within the Hosted application you’ll get the Application ID.

Now it’s time up update your Azure project, which is automatically created when you create the solution.  Here you’ll find 2 configuration files; ServiceConfiguration.cscfg and ServiceDefinition.csdef.

ServiceConfiguration.cscfg

<?xml version="1.0"?>
<ServiceConfiguration serviceName="Service" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration">
  <Role name="Web">
    <Instances count="1" />
    <ConfigurationSettings>
      <Setting name="AccountName" value="xxxxxxxxxxxxx"/>
      <Setting name="AccountSharedKey"    value="xxxxxxxxxxxx"/>
      <Setting name="TableStorageEndpoint" value="http://127.0.0.1:10002/"/>

    </ConfigurationSettings>
  </Role>
</ServiceConfiguration>

This file holds the configuration setting for accessing the service.  Above is only the local setting, once you deploy you’ll need to update these values to those generated in Azure.

ServiceDefinition.cscfg

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="xxxxxxxxxxx" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="Web">
    <InputEndpoints>
      <InputEndpoint name="HttpIn" protocol="http" port="80" />
    </InputEndpoints>
    <ConfigurationSettings>
      <Setting name="AccountName"/>
      <Setting name="AccountSharedKey"/>
      <Setting name="TableStorageEndpoint"/>
    </ConfigurationSettings>
  </WebRole>
</ServiceDefinition>

Project properties

image

Right clicking the Azure project and selecting the Portal tab and enter the Application ID which is available on the Azure site.

 

At this stage you’re ready to run everything, however I’m not going to describe that as you should be able to figure it out from here.

No comments: