So I started to wonder if it was possible to create class that implements ITableEntity and offer the dynamic features of an ExpandoObject. After a bit of hacking around in LinqPad I have this solution.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class ElasticTableEntity : DynamicObject, ITableEntity, | |
ICustomMemberProvider // For LinqPad's Dump | |
{ | |
public ElasticTableEntity() | |
{ | |
this.Properties = new Dictionary<string, EntityProperty>(); | |
} | |
public IDictionary<string, EntityProperty> Properties { get; private set; } | |
public object this[string key] | |
{ | |
get | |
{ | |
if (!this.Properties.ContainsKey(key)) | |
this.Properties.Add(key, this.GetEntityProperty(key, null)); | |
return this.Properties[key]; | |
} | |
set | |
{ | |
var property = this.GetEntityProperty(key, value); | |
if (this.Properties.ContainsKey(key)) | |
this.Properties[key] = property; | |
else | |
this.Properties.Add(key, property); | |
} | |
} | |
#region DynamicObject overrides | |
public override bool TryGetMember(GetMemberBinder binder, out object result) | |
{ | |
result = this[binder.Name]; | |
return true; | |
} | |
public override bool TrySetMember(SetMemberBinder binder, object value) | |
{ | |
this[binder.Name] = value; | |
return true; | |
} | |
#endregion | |
#region ITableEntity implementation | |
public string PartitionKey { get; set; } | |
public string RowKey { get; set; } | |
public DateTimeOffset Timestamp { get; set; } | |
public string ETag { get; set; } | |
public void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext) | |
{ | |
this.Properties = properties; | |
} | |
public IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext) | |
{ | |
return this.Properties; | |
} | |
#endregion | |
#region ICustomMemberProvider implementation for LinqPad's Dump | |
public IEnumerable<string> GetNames() | |
{ | |
return new[] { "PartitionKey", "RowKey", "Timestamp", "ETag" } | |
.Union(this.Properties.Keys); | |
} | |
public IEnumerable<Type> GetTypes() | |
{ | |
return new[] { typeof(string), typeof(string), typeof(DateTimeOffset), typeof(string) } | |
.Union(this.Properties.Values.Select(x => this.GetType(x.PropertyType))); | |
} | |
public IEnumerable<object> GetValues() | |
{ | |
return new object[] { this.PartitionKey, this.RowKey, this.Timestamp, this.ETag } | |
.Union(this.Properties.Values.Select(x => this.GetValue(x))); | |
} | |
#endregion | |
private EntityProperty GetEntityProperty(string key, object value) | |
{ | |
if (value == null) return new EntityProperty((string)null); | |
if (value.GetType() == typeof(byte[])) return new EntityProperty((byte[])value); | |
if (value.GetType() == typeof(bool)) return new EntityProperty((bool)value); | |
if (value.GetType() == typeof(DateTimeOffset)) return new EntityProperty((DateTimeOffset)value); | |
if (value.GetType() == typeof(DateTime)) return new EntityProperty((DateTime)value); | |
if (value.GetType() == typeof(double)) return new EntityProperty((double)value); | |
if (value.GetType() == typeof(Guid)) return new EntityProperty((Guid)value); | |
if (value.GetType() == typeof(int)) return new EntityProperty((int)value); | |
if (value.GetType() == typeof(long)) return new EntityProperty((long)value); | |
if (value.GetType() == typeof(string)) return new EntityProperty((string)value); | |
throw new Exception("not supported " + value.GetType() + " for " + key); | |
} | |
private Type GetType(EdmType edmType) | |
{ | |
switch (edmType) | |
{ | |
case EdmType.Binary : return typeof(byte[]); | |
case EdmType.Boolean : return typeof(bool); | |
case EdmType.DateTime : return typeof(DateTime); | |
case EdmType.Double : return typeof(double); | |
case EdmType.Guid : return typeof(Guid); | |
case EdmType.Int32 : return typeof(int); | |
case EdmType.Int64 : return typeof(long); | |
case EdmType.String : return typeof(string); | |
default: throw new Exception("not supported " + edmType); | |
} | |
} | |
private object GetValue(EntityProperty property) | |
{ | |
switch (property.PropertyType) | |
{ | |
case EdmType.Binary : return property.BinaryValue; | |
case EdmType.Boolean : return property.BooleanValue; | |
case EdmType.DateTime : return property.DateTimeOffsetValue; | |
case EdmType.Double : return property.DoubleValue; | |
case EdmType.Guid : return property.GuidValue; | |
case EdmType.Int32 : return property.Int32Value; | |
case EdmType.Int64 : return property.Int64Value; | |
case EdmType.String : return property.StringValue; | |
default: throw new Exception("not supported " + property.PropertyType); | |
} | |
} | |
} |
In this snippet I also implemented the ICustomMemberProvider which is part of the LinqPad extensions API for queries (more on this here). In Visual Studio we'll need to remove that code.
We can now use the ElasticTableEntity class like this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var connectionString="..."; | |
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString); | |
var client = storageAccount.CreateCloudTableClient(); | |
var table = client.GetTableReference("Demo"); | |
table.CreateIfNotExists(); | |
// dynamic keyword to use a dynamic entity | |
dynamic entity = new ElasticTableEntity(); | |
entity.PartitionKey = "Partition123"; | |
entity.RowKey = (DateTime.MaxValue.Ticks - DateTime.Now.Ticks).ToString(); | |
entity.Name = "Pascal"; | |
entity.Number = 34; | |
entity.Bool = false; | |
entity.Date = new DateTime(1912, 3, 4); | |
entity.TokenId = Guid.NewGuid(); | |
entity["LastName"] = "Laurin"; | |
// Insert the entity we created dynamically | |
table.Execute(TableOperation.Insert(entity)); | |
// Query all entities in the table | |
var query = new TableQuery<ElasticTableEntity>(); | |
var result = table.ExecuteQuery(query) | |
.ToList().Dump("Result"); | |
// Query only a subset of properties | |
var result2 = table.ExecuteQuery(query.Select(new[] { "FirstName", "Date" })) | |
.ToList().Dump("Result with projection"); |
Please note that you need to use the dynamic keyword to be able to define properties dynamically. You can also use the entity indexer like I did with the LastName property.
Result | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Result with projection | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
The ElasticTableEntity allows us to define properties at run time which will be added to the table when inserting the entities. Tables in the Azure Table Storage have flexible schema so we are free to store entities with different properties as long a we respect some limitations:
- Entities can have no more than 252 different properties (that's for the Table)
- An Entity's data can be up to 1 MB in size
- A property must be one of the following types : byte[], bool, DateTime, double, Guid, int, long or string
- A property value can be up to 64 KB in size (for string and byte array)
- A property name is case sensitive and can be no more than 255 characters in length
You can store about any kind of data as long as it is one of the supported data type. You could also encode other kind of date type in a byte array or a string (like a json document). Just be careful to always stick to one data type for a property (yes, we can store like int, bool and string in the same column using different entities!)
That's it for now. Next time I'll show you how to use the Windows Azure Table Storage Service as a document-oriented database with the ElasticTableEntity.
See also
- Document oriented database with Azure Table Storage Service- Using Azure Blob Storage to store documents
14 comments:
Great work!
Great work!
Just to let you know, I've implemented this on an Azure solution I've created (internal data tracking, nothing glamorous), and it works great. Something I added to the getter & setter is a cleaninput(key) method to strip out bad characters - Azure doesn't like Spaces, etc in Table column names. Maybe not the best place for it, but I don't have the opportunity to police the input all the time.
Thanks for the code share, though - helped a lot!
-Dan
Any ideas on Serialization? I added this:
[KnownType(typeof(ElasticTableEntity))]
[DataContract]
public class Root
{
[DataMember]
public string Name { get; set; }
[DataMember]
public dynamic Properties { get; set; }
}
[Serializable]
[DataServiceKey("PartitionKey", "RowKey")]
public class ElasticTableEntity : DynamicObject, ITableEntity, ISerializable
{
public static Boolean debug = false;
public ElasticTableEntity()
{
this.Properties = new Dictionary();
}
public IDictionary Properties { get; private set; }
...
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
foreach (var kvp in Properties)
{
info.AddValue(kvp.Key, kvp.Value);
}
}
But to no avail, still failing. Ideas?
@Dan are you trying to use ElasticTableEntity in a web service (or data service)? I usually don't re-use my *TableEntities for anything else than persistence in Table Storage. I never tried to serialize ElasticTableEntity but I suggest you manually handle serialization with attributes like [NonSerialized] and having explicte backing fields instead of auto-properties (especially for Dictionnaries which don't serialize well on their own IIRC).
I fought it for a few hours last night - the regular XML serializer won't work with IDictionaries, then I went the [DataContract] route with WCF serialization, and that crashed and burned and gave up - it wasn't a requirement, but a "want" for my project. I was trying to serialize an object to put it on the Message Queue for easy handing off to a WorkerRole. Blech. :-) Basically, DynamicEntities Don't Serialize, and I don't really need it to work.
I do, however, still like the concept for Table storage. It's nice. :-)
I am working on a NLog plugin for Azure Table Storage and was hoping to use your ElasticTableEntity. I'd add attribution at the top of the file for you. Let me know if you have any issues or want to handle things differently.
@Dan go ahead, I don't mind. Just mention this post it the code, especially if it's open source.
Very useful. Thanks man!
Removed the last post, clearly insufficient coffee and not enough sleep.
What I have been playing around with is extending this example to allow for it to support reading and writing simple POCO classes (with some additional mappings for things like enums). If anyone wants to assist, I can spin up a GitHub project and we can get cracking.
.//Adam
Adam I just finished adding enum support (stored as string values in table storage), set all ElasticTableEntity properties from public properties of a object, and create a new object setting its public props from ElasticTableEntity.
Did you ever create that github?
Azure Storage SDK > v8.0.0 solved the problem for writing complex objects into Table Storage. Please have a look at:
https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.storage.table.tableentity.flatten.aspx
https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.storage.table.tableentity.convertback.aspx
And the article I wrote about this before I checked the api s into the SDK:
https://doguarslan.wordpress.com/2016/02/03/writing-complex-objects-to-azure-table-storage/
Post a Comment