When working with any persistence layer you want to keep the infrastructure code separate from the business and UI logic, and working with Windows Azure Table Storage is no different. The WindowsAzure.Storage package provides a smooth API for working with tables, but not smooth enough to allow it into all areas of an application.
What I’d be looking for is an API as simple to use as the following.
var storage = new WidgetStorage(); var widgets= storage.GetAllForFacility("TERRITORY2", "FACILITY3"); foreach (var widget in widgets) { Console.WriteLine(widget.Name); }
The above code requires a little bit of work to abstract away connection details and query mechanics. First up is a base class for typed table storage access.
public class TableStorage<T> where T: ITableEntity, new() { public TableStorage(string tableName, string connectionName = "StorageConnectionString") { var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting(connectionName)); var tableClient = storageAccount.CreateCloudTableClient(); Table = tableClient.GetTableReference(tableName); Table.CreateIfNotExists(); } public virtual string Insert(T entity) { var operation = TableOperation.Insert(entity); var result = Table.Execute(operation); return result.Etag; } // update, merge, delete, insert many ... protected CloudTable Table; }
The base class can retrieve connection strings and abstract away TableOperation and BatchOperation work. It’s easy to extract an interface definition if you want to work with an abstract type. Meanwhile, derived classes can layer query operations into the mix.
public class WidgetStorage : TableStorage<Widget> { public WidgetStorage() : base(tableName: "widgets") { } public IEnumerable<Widget> GetAll() { var query = new AllWidgets(); return query.ExecuteOn(Table); } // ... public IEnumerable<Widget> GetAllForFacility(string territory, string facility) { var query = new AllWidgetsInFacility(territory, facility); return query.ExecuteOn(Table); } }
The actual query definitions I like to keep as separate classes.
public class AllWidgetsInFacility : StorageQuery<Widget> { public AllWidgetsInFacility(string territory, string facility) { Query = Query.Where(InclusiveRangeFilter( key: "PartitionKey", from: territory + "-" + facility, to: territory + "-" + facility + ".")); } }
Separate query classes allow a base class to focus on query execution, including the management of continuation tokens, timeout and retry policies, as well as query helper methods using TableQuery. The base class also allows for easy testability via the virtual ExecuteOn method.
public class StorageQuery<T> where T:TableEntity, new() { protected TableQuery<T> Query; public StorageQuery() { Query = new TableQuery<T>(); } public virtual IEnumerable<T> ExecuteOn(CloudTable table) { var token = new TableContinuationToken(); var segment = table.ExecuteQuerySegmented(Query, token); while (token != null) { foreach (var result in segment) { yield return result; } token = segment.ContinuationToken; segment = table.ExecuteQuerySegmented(Query, token); } } protected string InclusiveRangeFilter(string key, string from, string to) { var low = TableQuery.GenerateFilterCondition(key, QueryComparisons.GreaterThanOrEqual, from); var high = TableQuery.GenerateFilterCondition(key, QueryComparisons.LessThanOrEqual, to); return TableQuery.CombineFilters(low, TableOperators.And, high); } }
As an aside, one of the most useful posts on Azure Table storage is now almost 3 years old but contains many good nuggets of information. See: How to get (the) most out of Windows Azure Tables.
Thanks to Jon von Gillern for pointing out a bug. Fixed!