Azure – Part 5 – Repository Pattern for Table Service
- by Shaun
In my last post I created a very simple WCF service with the user registration functionality. I created an entity for the user data and a DataContext class which provides some methods for operating the entities such as add, delete, etc. And in the service method I utilized it to add a new entity into the table service. But I didn’t have any validation before registering which is not acceptable in a real project. So in this post I would firstly add some validation before perform the data creation code and show how to use the LINQ for the table service. LINQ to Table Service Since the table service utilizes ADO.NET Data Service to expose the data and the managed library of ADO.NET Data Service supports LINQ we can use it to deal with the data of the table service. Let me explain with my current example: I would like to ensure that when register a new user the email address should be unique. So I need to check the account entities in the table service before add. If you remembered, in my last post I mentioned that there’s a method in the TableServiceContext class – CreateQuery, which will create a IQueryable instance from a given type of entity. So here I would create a method under my AccountDataContext class to return the IQueryable<Account> which named Load. 1: public class AccountDataContext : TableServiceContext
2: {
3: private CloudStorageAccount _storageAccount;
4:
5: public AccountDataContext(CloudStorageAccount storageAccount)
6: : base(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials)
7: {
8: _storageAccount = storageAccount;
9:
10: var tableStorage = new CloudTableClient(_storageAccount.TableEndpoint.AbsoluteUri,
11: _storageAccount.Credentials);
12: tableStorage.CreateTableIfNotExist("Account");
13: }
14:
15: public void Add(Account accountToAdd)
16: {
17: AddObject("Account", accountToAdd);
18: SaveChanges();
19: }
20:
21: public IQueryable<Account> Load()
22: {
23: return CreateQuery<Account>("Account");
24: }
25: }
The method returns the IQueryable<Account> so that I can perform the LINQ operation on it. And back to my service class, I will use it to implement my validation.
1: public bool Register(string email, string password)
2: {
3: var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
4: var accountToAdd = new Account(email, password) { DateCreated = DateTime.Now };
5: var accountContext = new AccountDataContext(storageAccount);
6:
7: // validation
8: var accountNumber = accountContext.Load()
9: .Where(a => a.Email == accountToAdd.Email)
10: .Count();
11: if (accountNumber > 0)
12: {
13: throw new ApplicationException(string.Format("Your account {0} had been used.", accountToAdd.Email));
14: }
15:
16: // create entity
17: try
18: {
19: accountContext.Add(accountToAdd);
20: return true;
21: }
22: catch (Exception ex)
23: {
24: Trace.TraceInformation(ex.ToString());
25: }
26: return false;
27: }
I used the Load method to retrieve the IQueryable<Account> and use Where method to find the accounts those email address are the same as the one is being registered. If it has I through an exception back to the client side. Let’s run it and test from my simple client application.
Oops! Looks like we encountered an unexpected exception. It said the “Count” is not support by the ADO.NET Data Service LINQ managed library. That is because the table storage managed library (aka. TableServiceContext) is based on the ADO.NET Data Service and it supports very limit LINQ operation. Although I didn’t find a full list or documentation about which LINQ methods it supports I could even refer a page on msdn here. It gives us a roughly summary of which query operation the ADO.NET Data Service managed library supports and which doesn't. As you see the Count method is not in the supported list.
Not only the query operation, there inner lambda expression in the Where method are limited when using the ADO.NET Data Service managed library as well.
For example if you added (a => !a.DateDeleted.HasValue) in the Where method to exclude those deleted account it will raised an exception said "Invalid Input". Based on my experience you should always use the simple comparison (such as ==, >, <=, etc.) on the simple members (such as string, integer, etc.) and do not use any shortcut methods (such as string.Compare, string.IsNullOrEmpty etc.).
1: // validation
2: var accountNumber = accountContext.Load()
3: .Where(a => a.Email == accountToAdd.Email)
4: .ToList()
5: .Count;
6: if (accountNumber > 0)
7: {
8: throw new ApplicationException(string.Format("Your account {0} had been used.", accountToAdd.Email));
9: }
We changed the a bit and try again.
Since I had created an account with my mail address so this time it gave me an exception said that the email had been used, which is correct.
Repository Pattern for Table Service
The AccountDataContext takes the responsibility to save and load the account entity but only for that specific entity. Is that possible to have a dynamic or generic DataContext class which can operate any kinds of entity in my system? Of course yes. Although there's no typical database in table service we can threat the entities as the records, similar with the data entities if we used OR Mapping. As we can use some patterns for ORM architecture here we should be able to adopt the one of them - Repository Pattern in this example.
We know that the base class - TableServiceContext provide 4 methods for operating the table entities which are CreateQuery, AddObject, UpdateObject and DeleteObject. And we can create a relationship between the enmity class, the table container name and entity set name. So it's really simple to have a generic base class for any kinds of entities. Let's rename the AccountDataContext to DynamicDataContext and make the type of Account as a type parameter if it.
1: public class DynamicDataContext<T> : TableServiceContext where T : TableServiceEntity
2: {
3: private CloudStorageAccount _storageAccount;
4: private string _entitySetName;
5:
6: public DynamicDataContext(CloudStorageAccount storageAccount)
7: : base(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials)
8: {
9: _storageAccount = storageAccount;
10: _entitySetName = typeof(T).Name;
11:
12: var tableStorage = new CloudTableClient(_storageAccount.TableEndpoint.AbsoluteUri,
13: _storageAccount.Credentials);
14: tableStorage.CreateTableIfNotExist(_entitySetName);
15: }
16:
17: public void Add(T entityToAdd)
18: {
19: AddObject(_entitySetName, entityToAdd);
20: SaveChanges();
21: }
22:
23: public void Update(T entityToUpdate)
24: {
25: UpdateObject(entityToUpdate);
26: SaveChanges();
27: }
28:
29: public void Delete(T entityToDelete)
30: {
31: DeleteObject(entityToDelete);
32: SaveChanges();
33: }
34:
35: public IQueryable<T> Load()
36: {
37: return CreateQuery<T>(_entitySetName);
38: }
39: }
I saved the name of the entity type when constructed for performance matter. The table name, entity set name would be the same as the name of the entity class. The Load method returned a generic IQueryable instance which supports the lazy load feature. Then in my service class I changed the AccountDataContext to DynamicDataContext and that's all.
1: var accountContext = new DynamicDataContext<Account>(storageAccount);
Run it again and register another account.
The DynamicDataContext now can be used for any entities. For example, I would like the account has a list of notes which contains 3 custom properties: Account Email, Title and Content. We create the note entity class.
1: public class Note : TableServiceEntity
2: {
3: public string AccountEmail { get; set; }
4: public string Title { get; set; }
5: public string Content { get; set; }
6: public DateTime DateCreated { get; set; }
7: public DateTime? DateDeleted { get; set; }
8:
9: public Note()
10: : base()
11: {
12: }
13:
14: public Note(string email)
15: : base(email, string.Format("{0}_{1}", email, Guid.NewGuid().ToString()))
16: {
17: AccountEmail = email;
18: }
19: }
And no need to tweak the DynamicDataContext we can directly go to the service class to implement the logic. Notice here I utilized two DynamicDataContext instances with the different type parameters: Note and Account.
1: public class NoteService : INoteService
2: {
3: public void Create(string email, string title, string content)
4: {
5: var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
6: var accountContext = new DynamicDataContext<Account>(storageAccount);
7: var noteContext = new DynamicDataContext<Note>(storageAccount);
8:
9: // validate - email must be existed
10: var accounts = accountContext.Load()
11: .Where(a => a.Email == email)
12: .ToList()
13: .Count;
14: if (accounts <= 0)
15: throw new ApplicationException(string.Format("The account {0} does not exsit in the system please register and try again.", email));
16:
17: // save the note
18: var noteToAdd = new Note(email) { Title = title, Content = content, DateCreated = DateTime.Now };
19: noteContext.Add(noteToAdd);
20: }
21: }
And updated our client application to test the service.
I didn't implement any list service to show all notes but we can have a look on the local SQL database if we ran it at local development fabric.
Summary
In this post I explained a bit about the limited LINQ support for the table service. And then I demonstrated about how to use the repository pattern in the table service data access layer and make the DataContext dynamically. The DynamicDataContext I created in this post is just a prototype. In fact we should create the relevant interface to make it testable and for better structure we'd better separate the DataContext classes for each individual kind of entity. So it should have IDataContextBase<T>, DataContextBase<T> and for each entity we would have
class AccountDataContext<Account> : IDataContextBase<Account>, DataContextBase<Account> { … }
class NoteDataContext<Note> : IDataContextBase<Note>, DataContextBase<Note> { … }
Besides the structured data saving and loading, another common scenario would be saving and loading some binary data such as images, files. In my next post I will show how to use the Blob Service to store the bindery data - make the account be able to upload their logo in my example.
Hope this helps,
Shaun
All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.