Upgrading Table Storage Code to Azure SDK 1.8

Auzre SDK 1.8 is out and you can now target .Net 4.5 and use Windows Server 2012 in Azure. I wanted to upgrade my code but I found very little guidance on how to do it. So here's a quick example of some table storage code I had written and how I changed it to work with the new SDK.

Originally, you had to install the SDK and add references to Microsoft.WindowsAzure.StorageClient, System.Data.DataSetExtensions, and System.Data.Services.Client. Then you need to create a subclass of TableServiceEntity to hold the data you want to put in the table. Then it was up to you whether you wanted to create your own subclass of TableServiceContext.

My original code is not perfect but functional. Here is the whole helper class:

 using System;
using System.Configuration;
using System.Data.Services.Client;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

public class AzureTableHelper
{
    CloudStorageAccount storageAccount = null;
    string tableEndpoint;

    const string TableName = "testruns";

    public class Run : TableServiceEntity
    {
        public Run() 
            : this(Guid.NewGuid(), RunStatus.Unknown) { }

        public Run(Guid runId, RunStatus status) 
            : this(runId.ToString(), status.ToString(), runId.ToString() + ".xml") { }

        public Run(string runId, string status, string outputBlob)
            : base(TableName, runId)
        {
            this.RunId = runId;
            this.Status = status;
            this.OutputBlob = outputBlob;
        }

        public string RunId { get; set; }
        public string Status { get; set; }
        public string OutputBlob { get; set; }
    }

    public class RunsContext : TableServiceContext
    {
        public RunsContext(string baseAddress, StorageCredentials credentials)
            : base(baseAddress, credentials) { }

        public IQueryable<Run> Runs
        {
            get { return this.CreateQuery<Run>(TableName); }
        }

        public void AddRun(Run run)
        {
            this.AddObject(TableName, run);
            this.SaveChangesWithRetries(SaveChangesOptions.ReplaceOnUpdate);
        }

        public void UpdateRun(Run run)
        {
            this.UpdateObject(run);
            this.SaveChangesWithRetries(SaveChangesOptions.ReplaceOnUpdate);
        }
    }

    public AzureTableHelper()
    {
        this.tableEndpoint = ConfigurationManager.AppSettings["StorageTableEndpoint"];
        string accountName = ConfigurationManager.AppSettings["StorageAccountName"];
        string accountKey = ConfigurationManager.AppSettings["StorageAccountKey"];

        this.storageAccount = CloudStorageAccount.Parse(
            string.Format("DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}", accountName, accountKey)
            );
        CloudTableClient cloudTableClient = this.storageAccount.CreateCloudTableClient();
        cloudTableClient.CreateTableIfNotExist(TableName);
    }

    public void AddRun(Guid guid, RunStatus runStatus)
    {
        RunsContext runsContext = new RunsContext(this.tableEndpoint, this.storageAccount.Credentials);
        runsContext.AddRun(new Run(guid, runStatus));
    }

    public void UpdateRun(Guid guid, RunStatus runStatus)
    {
        RunsContext runsContext = new RunsContext(this.tableEndpoint, this.storageAccount.Credentials);
        var runQuery = from r in runsContext.CreateQuery<Run>(TableName)
                       where r.RunId == guid.ToString()
                       select r;
        Run run = null;
        int retries = 0;
        while (retries < 3)
        {
            try
            {
                run = runQuery.FirstOrDefault();
                break;
            }
            catch
            {
                retries++;
            }
        }

        if (run != null)
        {
            run.Status = runStatus.ToString();
            runsContext.UpdateRun(run);
        }
    }

    public Run GetRun(Guid guid)
    {
        RunsContext runsContext = new RunsContext(this.tableEndpoint, this.storageAccount.Credentials);
        var runQuery = from r in runsContext.CreateQuery<Run>(TableName)
                       where r.RunId == guid.ToString()
                       select r;
        Run run = null;
        int retries = 0;
        while (retries < 3)
        {
            try
            {
                run = runQuery.FirstOrDefault();
                break;
            }
            catch
            {
                retries++;
            }
        }

        return run;
    }
}

Notice that in the update and get methods I execute a query to get the particular Run data I'm looking for. This may not have been the best way to do it but it worked so I didn't mess with it.

In the 1.8 SDK, it is advisable to remove the context. This means you can also remove the references for System.Data.DataSetExtensions and System.Data.Services.Client. Instead of adding a reference to Microsoft.WindowsAzure.StorageClient, use NuGet to grab Windows Azure Storage version 2.0.0.0.

Instead of using TableServiceEntity, use TableEntity as the base class for Run. CloudTableClient no longer has the CreateTableIfNotExist method. Instead, you have to get a CloudTable object and call CreateIfNotExists.

 using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.RetryPolicies;
using Microsoft.WindowsAzure.Storage.Table;
using System;

public class AzureTableHelper
{
    CloudStorageAccount storageAccount;
    CloudTableClient cloudTableClient;
    CloudTable cloudTable;

    const string TableName = "testruns";

    public class Run : TableEntity
    {
        public Run() 
            : this(Guid.NewGuid(), RunStatus.Unknown) { }

        public Run(Guid runId, RunStatus status) 
            : this(runId.ToString(), status.ToString(), runId.ToString() + ".xml") { }

        public Run(string runId, string status, string outputBlob)
            : base(TableName, runId)
        {
            this.RunId = runId;
            this.Status = status;
            this.OutputBlob = outputBlob;
        }

        public string RunId { get; set; }
        public string Status { get; set; }
        public string OutputBlob { get; set; }
    }

    public AzureTableHelper()
    {
        string accountName = CloudConfigurationManager.GetSetting("StorageAccountName");
        string accountKey = CloudConfigurationManager.GetSetting("StorageAccountKey");

        this.storageAccount = new CloudStorageAccount(
            new StorageCredentials(accountName, accountKey), true);

        this.cloudTableClient = this.storageAccount.CreateCloudTableClient();
        this.cloudTable = this.cloudTableClient.GetTableReference(TableName);
        this.cloudTable.CreateIfNotExists();
    }

    public void AddRun(Guid guid, RunStatus runStatus)
    {
        this.cloudTable.Execute(
            TableOperation.Insert(new Run(guid, runStatus)), 
            new TableRequestOptions() 
                { RetryPolicy = new LinearRetry(TimeSpan.FromSeconds(3), 3) });
    }

    public void UpdateRun(Guid guid, RunStatus runStatus)
    {
        Run run = this.GetRun(guid);
        if (run != null)
        {
            run.Status = runStatus.ToString();
            this.cloudTable.Execute(TableOperation.Replace(run));
        }
    }

    public Run GetRun(Guid guid)
    {
        TableResult tableResult = this.cloudTable.Execute(
            TableOperation.Retrieve<Run>(TableName, guid.ToString()));
        return tableResult.Result as Run;
    }
}

The key difference is that I'm using CloudTable.Execute to do my operations instead of the context.

UPDATE - Previously I had written this code using the EntityResolver. That's actually not necessary if you're using a TableEntity. Here is the original code I wrote using the EntityResolver:

     public Run GetRun(Guid guid)
    {
        TableResult tableResult = this.cloudTable.Execute(
            TableOperation.Retrieve<Run>(
                TableName, 
                guid.ToString(), 
                new EntityResolver<Run>(
                    (partitionKey, rowKey, timeStamp, properties, etag) =>
                    {
                        return new Run(
                            rowKey, 
                            properties["Status"].StringValue, 
                            properties["OutputBlob"].StringValue) 
                            { ETag = etag, Timestamp = timeStamp };
                    })));
        return tableResult.Result as Run;
    }

I tried searching for the EntityResolver class and only found references to the Azure Java SDK. The ETag property is key though. If you don't assign an ETag, you can't use the Replace operation.