Given all latest changes to SDKs and tools, I think a significant update is needed for my old blog post on creating a data access layer for Azure Table Storage in Silverlight (Porting Silverlight RIA to Windows Azure - Part 1). It is now a part of whitepaper I'm working on, soon to be released on MSDN.

Note: Steps 1-3 represent one server-side model class, step 4 represents server-side data service class and steps 5-7 represent third, client-side view model class.

Step 1. Create POCO classes to represent your data model


Without EF-generated entities, one can create data model manually with help of plain old CLR objects (POCO) that look like this:

public class Report : TableServiceEntity

{

      public Report()

      {

      }

 

      public Report(string title, string description, string initiatorId, string solverId, string attachments)

            : base(title, title)

      {

            Title = title;

            Description = description;

            InitiatorId = initiatorId;

            SolverId = solverId;

            Attachments = attachments;

            DateCreated = DateTime.UtcNow;

            ReportId = Guid.NewGuid();

      }

 

      public string Title { get; set; }

      public string Description { get; set; }

      public string Solution { get; set; }

      public string InitiatorId { get; set; }

      public string SolverId { get; set; }

      public string Attachments { get; set; }

      public DateTime DateCreated { get; set; }

      public Guid ReportId { get; set; }

}


Please note the use of TableServiceEntity as a base class – it is a class from built-in Windows Azure StorageClient library representing an entity in ATS. Among other things it adds ATS-specific attributes such as definitions of RowKey and PartitionKey to these entity classes.

Step 2. Create a data model class to be used with ADO.NET DS

 

Now, we need to wrap our entities into a model class that will serve as a representation of our entities visible to all clients. This not only allows us to separate the database representation of the entities from what we want to expose to the clients but also allows us to take advantage of StorageClient’s TableServiceContext class that will be used later by WCF DS.

public partial class AzureModel : TableServiceContext

{

      private const string ReportsTableName = "Reports";

 

      public AzureModel(string baseAddress, StorageCredentials credentials) : base(baseAddress, credentials)

               {

               }

 

      public IQueryable<Report> Reports

                {

            get { return CreateQuery<Report>(ReportsTableName); }

      }

...

 

Step 3. Provide implementation for IUpdatable

 

The reason you need IUpdatable implementation is because ADO.NET uses it to update the storage.  The ADO.NET Entity Framework's ObjectContext does not implement IUpdatable, but its ObjectContextServiceProvider does, so building a Data Service with an ObjectContext gives you an update support for free; using LINQ to SQL with a DataContext results in using the ReflectionServiceProvider that has no built in support for IUpdatable operations and relies on the data service context to implement it; same applies to any other DataServiceContext-based context classes such as StorageClient’s TableServiceContext used here. Therefore, we need to create our own IUpdatable implementation. After that is done, you need to do is to inherit your model class from it.

public partial class AzureModel : IUpdatable

{

      public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)

      {

            AddLink(targetResource, propertyName, resourceToBeAdded);

      }

 

      public void ClearChanges()

      {

            // clear out links

            foreach (LinkDescriptor link in Links)

            {

                  DetachLink(link.Source, link.SourceProperty, link.Target);

            }

 

            // clear out entities

            foreach (EntityDescriptor entity in Entities)

            {

                  Detach(entity.Entity);

            }

      }

 

      public object CreateResource(string containerName, string fullTypeName)

      {

            object obj = Activator.CreateInstance(GetType().Assembly.GetType(fullTypeName));

            AddObject(containerName, obj);

            return obj;

      }

 

      public void DeleteResource(object targetResource)

      {

            DeleteObject(targetResource);

      }

 

      public object GetResource(IQueryable query, string fullTypeName)

      {

            object resource;

 

            // fullTypeName can be null for deletes

            if (fullTypeName == null)

            {

                  resource = query.Cast<BaseEntity>().SingleOrDefault();

            }

            else

            {

                  // Check for types that will be updated or deleted

                  if (fullTypeName == typeof(Report).FullName)

                  {

                        resource = query.Cast<Report>().SingleOrDefault();

                  }

                  UpdateObject(resource);

            }

 

            return resource;

      }

 

      public object GetValue(object targetResource, string propertyName)

      {

            Type type = targetResource.GetType();

            PropertyInfo propInfo = type.GetProperty(propertyName);

            if (propInfo == null)

            {

                  throw new Exception(string.Format("Can't find given property '{0}'.", propertyName ?? "NULL"));

            }

            object val = propInfo.GetValue(targetResource, null);

            return val;

      }

 

      public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)

      {

            DeleteLink(targetResource, propertyName, resourceToBeRemoved);

      }

 

      public object ResetResource(object resource)

      {

            Detach(resource);

            return resource;

      }

 

      public object ResolveResource(object resource)

      {

            // Already gave object in CreateResource

            return resource;

      }

 

      void IUpdatable.SaveChanges()

      {

            try

            {

                  SaveChanges();

            }

            catch (KeyNotFoundException e)

            {

                  var exception = e;

            }

      }

 

      public void SetReference(object targetResource, string propertyName, object propertyValue)

      {

            SetLink(targetResource, propertyName, propertyValue);

      }

 

      public void SetValue(object targetResource, string propertyName, object propertyValue)

      {

            PropertyInfo propInfo = targetResource.GetType().GetProperty(propertyName);

            if (propInfo == null)

            {

                  throw new Exception(string.Format("Can't find property '{0}'", propertyName ?? String.Empty));

            }

            propInfo.SetValue(targetResource, propertyValue, null);

      }

}

 

The overall flow of data is shown in Figure 1.  Please note that we are calling DataServiceContext.UpdateObject() when we need to “synchronize” two contexts – client and server – as they don’t automatically update each other.

 

Figure 1. Accessing data in ATS via WCF DS

Please note that we are updating the object when it is being requested to accommodate all changes that occured on the client to this object as they don’t automatically propagate to the other client context we use to access data in ATS.

 

Step 4. Create Data Service in a web role

 

Add new ADO.NET Data Service item to your Azure web role project. You’ll have to reference your DAL project to have access to the model and then you can base your data service on it:

public class AzureDS : DataService<AzureModel>

{

      public static void InitializeService(IDataServiceConfiguration config)

      {

            Logger.Info("InitializeService");

            try

            {

                  config.SetEntitySetAccessRule("*", EntitySetRights.All);

                  config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);

            }

            catch (Exception e)

            {

                  Logger.Error(e.Message);

            }

      }

      protected override AzureModel CreateDataSource()

      {

            var cloudStorageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");

            string baseAddress =    cloudStorageAccount.TableEndpoint.ToString();

            StorageCredentials credentials = cloudStorageAccount.Credentials;

           

            var azureModel = new AzureModel(baseAddress, credentials);

           

            return azureModel;

      }

}

 

Step 5. Add a service reference


Now, add a service reference to the data service in your Silverlight project as you would do with any other web service:

 

 Figure 2. Adding a service reference to WCF Data Service

Step 6. Create a view model


In view model class, you need to initialize data context with service URI. It’s a good idea to abstract the data service context with help of object property:

protected AzureModel Context

{

      get

      {

            if (_model == null)

            {

                  _model = new AzureModel(new Uri("AzureDS.svc", UriKind.Relative))

                  {

                        MergeOption = MergeOption. PreserveChanges

                  };

            }

            return _model;

      }

}

 

Please note - you might want to switch between NoTracking and PreserveChanges merge options here to achieve maximum performance of the queries as NoTracking is faster but can be used safely only when there are no changes being made in the client context. You can find more about various optimizations in the next chapter.

 

Step 7. Populate view model with service operations


Modify all your Silverlight code so that it uses LINQ to SQL to query objects and uses DataServiceContext’s BeginSaveChanges to make updates.

public void CreateReport(string liveUserId, string title, string description,   string categoryId)

{

      //Associate with current user

      var query = (DataServiceQuery<User>)(from c in Context.Users

                                     where c.RowKey == liveUserId

                                     select c);

      // Execute the query

      query.BeginExecute(a =>

            {

                  try

                  {

                        _matchingUsers = query.EndExecute(a).Select(user => (IUserElement)new UserElement(user));

 

                        IUserElement userElement = _matchingUsers.FirstOrDefault();

 

                        var report = new Report

                              {

                                    Title = title,

                                    Description = description,

                                    InitiatorId = userElement.LiveUserId,

                                    DateCreated = GetServerDateTime(),

                                    CategoryId = categoryId,

                                    Solution = String.Empty,

                                    SolverId = String.Empty,

                                    Attachments = String.Empty

                              };

 

                        report.RowKey = string.Format("{0:D19}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks);

                        report.PartitionKey = report.DateCreated.ToString("yyyy-MM");

                        report.ReportId = Guid.NewGuid();

 

                        Context.AddToReports(report);

                        Context.BeginSaveChanges(CreateReport_EndSaveChanges, report);

                  }

                  catch (Exception ex)

                  {

                        RaiseErrorHandler(new ErrorEventArgs(ex));

                  }

            },

            null);

}


Step 8. Call EndSaveChanges to persist changes to ATS


private void CreateReport_EndSaveChanges(IAsyncResult result)

{

      try

      {

            Context.EndSaveChanges(result);

            CreateReportCompleted(this, new CreateReportEventArgs(((Report)result.AsyncState)));

      }

      catch (DataServiceRequestException ex)

      {

            HtmlPage.Window.Alert("Error occurred while creating report: " + ex.Response);

            RaiseErrorHandler(new ErrorEventArgs(ex));

      }

      catch (Exception ex)

      {

            HtmlPage.Window.Alert("Error occurred while creating report: " + ex.Message);

            RaiseErrorHandler(new ErrorEventArgs(ex));

      }

}

 

Now, all you have to do is to wire up your view model with your views and you can safely use ATS data in Silverlight and enjoy benefits of loosely coupled MVVM implementation.