diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Net45/CoreHelpers.WindowsAzure.Storage.Table.Net45.csproj b/CoreHelpers.WindowsAzure.Storage.Table.Net45/CoreHelpers.WindowsAzure.Storage.Table.Net45.csproj
index b3ad871..284c4b6 100644
--- a/CoreHelpers.WindowsAzure.Storage.Table.Net45/CoreHelpers.WindowsAzure.Storage.Table.Net45.csproj
+++ b/CoreHelpers.WindowsAzure.Storage.Table.Net45/CoreHelpers.WindowsAzure.Storage.Table.Net45.csproj
@@ -77,6 +77,9 @@
Attributes\PartitionKeyAttribute.cs
+
+ Attributes\RelatedTableAttribute.cs
+
Attributes\RowKeyAttribute.cs
@@ -101,6 +104,9 @@
Attributes\VirtualTypeAttribute.cs
+
+ DynamicLazy.cs
+
DynamicTableEntity.cs
@@ -116,9 +122,15 @@
Extensions\PropertyInfoSetValueFromEntityProperty.cs
+
+ Extensions\TableQueryEx.cs
+
PagedTableEntityWriter.cs
+
+ RelatedTableItem.cs
+
StorageContext.cs
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs
new file mode 100644
index 0000000..b306f7d
--- /dev/null
+++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes
+{
+ public class RelatedTableAttribute : Attribute
+ {
+ ///
+ /// The partitionkey of the related table, if this is the name of a property on the model the property value will be used.
+ ///
+ public string PartitionKey { get; set; }
+
+ ///
+ /// The rowkey of the related table, if this is a property on the model, the property value will be loaded, if it is empty this will default to the name of the type.
+ ///
+ public string RowKey { get; set; }
+
+ ///
+ ///
+ ///
+ /// The partitionkey of the related table, if this is the name of a property on the model the property value will be used.
+ public RelatedTableAttribute(string partitionKey)
+ {
+ PartitionKey = partitionKey;
+ }
+
+ ///
+ ///
+ ///
+ /// The partitionkey of the related table, if this is the name of a property on the model the property value will be used.
+ /// The rowkey of the related table, if this is a property on the model, the property value will be loaded, if it is empty this will default to the name of the type.
+ public RelatedTableAttribute(string partitionKey, string rowKey)
+ {
+ PartitionKey = partitionKey;
+ RowKey = rowKey;
+ }
+
+ }
+}
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj
index 719351b..73d3f07 100644
--- a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj
+++ b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj
@@ -11,8 +11,6 @@
-
-
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/DynamicLazy.cs b/CoreHelpers.WindowsAzure.Storage.Table/DynamicLazy.cs
new file mode 100644
index 0000000..774b2ec
--- /dev/null
+++ b/CoreHelpers.WindowsAzure.Storage.Table/DynamicLazy.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace CoreHelpers.WindowsAzure.Storage.Table
+{
+ internal class DynamicLazy : Lazy
+ {
+ public DynamicLazy(Func factory) : base(() => (T)factory())
+ {
+
+ }
+
+ }
+}
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntity.cs b/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntity.cs
index 362bd39..069ab50 100644
--- a/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntity.cs
+++ b/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntity.cs
@@ -124,9 +124,16 @@ internal static bool ShouldSkipProperty(PropertyInfo property, OperationContext
{
// Logger.LogInformational(operationContext, SR.TraceIgnoreAttribute, property.Name);
return true;
- }
+ }
+
+ // properties with [RelatedTable]
+ if (property.GetCustomAttribute(typeof(RelatedTableAttribute)) != null)
+ {
+ // Logger.LogInformational(operationContext, SR.TraceIgnoreAttribute, property.Name);
+ return true;
+ }
- return false;
+ return false;
}
private static IDictionary ReflectionWrite(object entity, OperationContext operationContext)
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/PropertyInfoExtension.cs b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/PropertyInfoExtension.cs
index 77c7296..2f04b22 100644
--- a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/PropertyInfoExtension.cs
+++ b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/PropertyInfoExtension.cs
@@ -24,5 +24,19 @@ public static void SetOrAddValue(this PropertyInfo propertyInfo, object obj, obj
} else
propertyInfo.SetValue(obj, val, null);
}
- }
+
+ public static bool IsGenericOfType(this Type toCheck, Type generic)
+ {
+ while (toCheck != null && toCheck != typeof(object))
+ {
+ var cur = toCheck.GetTypeInfo().IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
+ if (generic == cur)
+ {
+ return true;
+ }
+ toCheck = toCheck.GetTypeInfo().BaseType;
+ }
+ return false;
+ }
+ }
}
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableQueryEx.cs b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableQueryEx.cs
new file mode 100644
index 0000000..dd30446
--- /dev/null
+++ b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableQueryEx.cs
@@ -0,0 +1,26 @@
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace CoreHelpers.WindowsAzure.Storage.Table.Extensions
+{
+ public class TableQueryEx
+ {
+ public static string CheckAndCombineFilters(string filterA, string operatorString, string filterB)
+ {
+ if (string.IsNullOrWhiteSpace(filterA))
+ return filterB;
+
+ return TableQuery.CombineFilters(filterA, operatorString, filterB);
+ }
+
+ public static string CombineFilters(IEnumerable filters, string operatorString)
+ {
+ return string.Join(
+ " " + operatorString + " ",
+ filters
+ );
+ }
+ }
+}
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/RelatedTableItem.cs b/CoreHelpers.WindowsAzure.Storage.Table/RelatedTableItem.cs
new file mode 100644
index 0000000..d4f6af4
--- /dev/null
+++ b/CoreHelpers.WindowsAzure.Storage.Table/RelatedTableItem.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace CoreHelpers.WindowsAzure.Storage.Table
+{
+ internal class RelatedTableItem where T : new()
+ {
+ public string RowKey { get; set; }
+ public string PartitionKey { get; set; }
+
+ public DynamicTableEntity Model { get; set; }
+
+ public PropertyInfo Property { get; set; }
+ }
+}
diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs
index e5612e5..32d9f90 100644
--- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs
+++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs
@@ -7,6 +7,8 @@
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Table;
using CoreHelpers.WindowsAzure.Storage.Table.Attributes;
+using CoreHelpers.WindowsAzure.Storage.Table.Extensions;
+using HandlebarsDotNet;
namespace CoreHelpers.WindowsAzure.Storage.Table
{
@@ -28,131 +30,139 @@ public class QueryResult
public class StorageContext : IDisposable
{
- private CloudStorageAccount _storageAccount { get; set; }
- private Dictionary _entityMapperRegistry { get; set; } = new Dictionary();
- private bool _autoCreateTable { get; set; } = false;
- private IStorageContextDelegate _delegate { get; set; }
+ private CloudStorageAccount _storageAccount { get; set; }
+ private Dictionary _entityMapperRegistry { get; set; } = new Dictionary();
+ private bool _autoCreateTable { get; set; } = false;
+ private IStorageContextDelegate _delegate { get; set; }
- public StorageContext(string storageAccountName, string storageAccountKey, string storageEndpointSuffix = null)
- {
- var connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2}", "https", storageAccountName, storageAccountKey);
- if (!String.IsNullOrEmpty(storageEndpointSuffix))
- connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2};EndpointSuffix={3}", "https", storageAccountName, storageAccountKey, storageEndpointSuffix);
+ public StorageContext(string storageAccountName, string storageAccountKey, string storageEndpointSuffix = null)
+ {
+ var connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2}", "https", storageAccountName, storageAccountKey);
+ if (!String.IsNullOrEmpty(storageEndpointSuffix))
+ connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2};EndpointSuffix={3}", "https", storageAccountName, storageAccountKey, storageEndpointSuffix);
- _storageAccount = CloudStorageAccount.Parse(connectionString);
- }
-
- public StorageContext(StorageContext parentContext)
- {
- _storageAccount = parentContext._storageAccount;
- _entityMapperRegistry = new Dictionary(parentContext._entityMapperRegistry);
- this.SetDelegate(parentContext._delegate);
- }
-
- public void Dispose()
- {
-
- }
-
- public void SetDelegate(IStorageContextDelegate delegateModel)
- {
- _delegate = delegateModel;
- }
-
- public StorageContext EnableAutoCreateTable()
- {
- _autoCreateTable = true;
- return this;
- }
+ _storageAccount = CloudStorageAccount.Parse(connectionString);
+ }
+
+ public StorageContext(string connectionString)
+ {
+ _storageAccount = CloudStorageAccount.Parse(connectionString);
+ }
+
+ public StorageContext(StorageContext parentContext)
+ {
+ _storageAccount = parentContext._storageAccount;
+ _entityMapperRegistry = new Dictionary(parentContext._entityMapperRegistry);
+ this.SetDelegate(parentContext._delegate);
+ }
+
+ public void Dispose()
+ {
- public void AddEntityMapper(Type entityType, DynamicTableEntityMapper entityMapper)
- {
- _entityMapperRegistry.Add(entityType, entityMapper);
- }
+ }
+
+ public void SetDelegate(IStorageContextDelegate delegateModel)
+ {
+ _delegate = delegateModel;
+ }
+
+ public StorageContext EnableAutoCreateTable()
+ {
+ _autoCreateTable = true;
+ return this;
+ }
+
+ public void AddEntityMapper(Type entityType, DynamicTableEntityMapper entityMapper)
+ {
+ _entityMapperRegistry.Add(entityType, entityMapper);
+ }
public void RemoveEntityMapper(Type entityType)
{
if (_entityMapperRegistry.ContainsKey(entityType))
_entityMapperRegistry.Remove(entityType);
}
-
- public void AddAttributeMapper()
+
+ public void AddAttributeMapper()
{
AddAttributeMapper(Assembly.GetEntryAssembly());
/*foreach(var assembly in Assembly.GetEntryAssembly().GetReferencedAssemblies()) {
AddAttributeMapper(assembly);
- } */
+ } */
}
internal void AddAttributeMapper(Assembly assembly)
{
var typesWithAttribute = assembly.GetTypesWithAttribute(typeof(StorableAttribute));
- foreach(var type in typesWithAttribute) {
+ foreach (var type in typesWithAttribute)
+ {
AddAttributeMapper(type);
}
}
-
- public void AddAttributeMapper(Type type)
+
+ public void AddAttributeMapper(Type type)
{
- AddAttributeMapper(type, string.Empty);
+ AddAttributeMapper(type, string.Empty);
}
-
- public void AddAttributeMapper(Type type, String optionalTablenameOverride )
+
+ public void AddAttributeMapper(Type type, String optionalTablenameOverride)
{
- // get the concrete attribute
- var storableAttribute = type.GetTypeInfo().GetCustomAttribute();
- if (String.IsNullOrEmpty(storableAttribute.Tablename)) {
+ // get the concrete attribute
+ var storableAttribute = type.GetTypeInfo().GetCustomAttribute();
+ if (String.IsNullOrEmpty(storableAttribute.Tablename))
+ {
storableAttribute.Tablename = type.Name;
}
// store the neded properties
string partitionKeyFormat = null;
string rowKeyFormat = null;
-
+
// get the partitionkey property & rowkey property
var properties = type.GetRuntimeProperties();
foreach (var property in properties)
{
if (partitionKeyFormat != null && rowKeyFormat != null)
break;
-
+
if (partitionKeyFormat == null && property.GetCustomAttribute() != null)
partitionKeyFormat = property.Name;
-
- if (rowKeyFormat == null && property.GetCustomAttribute() != null)
- rowKeyFormat = property.Name;
+
+ if (rowKeyFormat == null && property.GetCustomAttribute() != null)
+ rowKeyFormat = property.Name;
}
- // virutal partition key property
- var virtualPartitionKeyAttribute = type.GetTypeInfo().GetCustomAttribute();
- if (virtualPartitionKeyAttribute != null && !String.IsNullOrEmpty(virtualPartitionKeyAttribute.PartitionKeyFormat))
- partitionKeyFormat = virtualPartitionKeyAttribute.PartitionKeyFormat;
-
- // virutal row key property
- var virtualRowKeyAttribute = type.GetTypeInfo().GetCustomAttribute();
- if (virtualRowKeyAttribute != null && !String.IsNullOrEmpty(virtualRowKeyAttribute.RowKeyFormat))
- rowKeyFormat = virtualRowKeyAttribute.RowKeyFormat;
-
+ // virutal partition key property
+ var virtualPartitionKeyAttribute = type.GetTypeInfo().GetCustomAttribute();
+ if (virtualPartitionKeyAttribute != null && !String.IsNullOrEmpty(virtualPartitionKeyAttribute.PartitionKeyFormat))
+ partitionKeyFormat = virtualPartitionKeyAttribute.PartitionKeyFormat;
+
+ // virutal row key property
+ var virtualRowKeyAttribute = type.GetTypeInfo().GetCustomAttribute();
+ if (virtualRowKeyAttribute != null && !String.IsNullOrEmpty(virtualRowKeyAttribute.RowKeyFormat))
+ rowKeyFormat = virtualRowKeyAttribute.RowKeyFormat;
+
// check
if (partitionKeyFormat == null || rowKeyFormat == null)
throw new Exception("Missing Partition or RowKey Attribute");
-
+
// build the mapper
AddEntityMapper(type, new DynamicTableEntityMapper()
{
TableName = String.IsNullOrEmpty(optionalTablenameOverride) ? storableAttribute.Tablename : optionalTablenameOverride,
PartitionKeyFormat = partitionKeyFormat,
RowKeyFormat = rowKeyFormat
- });
- }
-
- public IEnumerable GetRegisteredMappers()
+ });
+ }
+
+ public IEnumerable GetRegisteredMappers()
{
- return _entityMapperRegistry.Keys;
+ return _entityMapperRegistry.Keys;
}
- public void OverrideTableName(string tableName) {
+ public void OverrideTableName(string tableName)
+ {
OverrideTableName(typeof(T), tableName);
}
@@ -161,36 +171,36 @@ public void OverrideTableName(Type entityType, string tableName)
if (_entityMapperRegistry.ContainsKey(entityType))
_entityMapperRegistry[entityType].TableName = tableName;
}
-
- public Task CreateTableAsync(Type entityType, bool ignoreErrorIfExists = true)
- {
- // Retrieve a reference to the table.
- CloudTable table = GetTableReference(GetTableName(entityType));
-
- if (ignoreErrorIfExists)
- {
- // Create the table if it doesn't exist.
- return table.CreateIfNotExistsAsync();
- }
- else
- {
- // Create table and throw error
- return table.CreateAsync();
- }
- }
-
-
- public Task CreateTableAsync(bool ignoreErrorIfExists = true)
- {
- return CreateTableAsync(typeof(T), ignoreErrorIfExists);
- }
-
- public void CreateTable(bool ignoreErrorIfExists = true)
- {
- this.CreateTableAsync(ignoreErrorIfExists).GetAwaiter().GetResult();
- }
- public async Task DropTableAsync(Type entityType, bool ignoreErrorIfNotExists = true)
+ public Task CreateTableAsync(Type entityType, bool ignoreErrorIfExists = true)
+ {
+ // Retrieve a reference to the table.
+ CloudTable table = GetTableReference(GetTableName(entityType));
+
+ if (ignoreErrorIfExists)
+ {
+ // Create the table if it doesn't exist.
+ return table.CreateIfNotExistsAsync();
+ }
+ else
+ {
+ // Create table and throw error
+ return table.CreateAsync();
+ }
+ }
+
+
+ public Task CreateTableAsync(bool ignoreErrorIfExists = true)
+ {
+ return CreateTableAsync(typeof(T), ignoreErrorIfExists);
+ }
+
+ public void CreateTable(bool ignoreErrorIfExists = true)
+ {
+ this.CreateTableAsync(ignoreErrorIfExists).GetAwaiter().GetResult();
+ }
+
+ public async Task DropTableAsync(Type entityType, bool ignoreErrorIfNotExists = true)
{
// Retrieve a reference to the table.
CloudTable table = GetTableReference(GetTableName(entityType));
@@ -206,269 +216,463 @@ public async Task DropTableAsync(bool ignoreErrorIfNotExists = true)
await DropTableAsync(typeof(T), ignoreErrorIfNotExists);
}
- public void DropTable(bool ignoreErrorIfNotExists = true)
+ public void DropTable(bool ignoreErrorIfNotExists = true)
{
Task.Run(async () => await DropTableAsync(typeof(T), ignoreErrorIfNotExists)).Wait();
}
- public async Task InsertAsync(IEnumerable models) where T : new ()
- {
- await this.StoreAsync(nStoreOperation.insertOperation, models);
- }
+ public async Task InsertAsync(IEnumerable models) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.insertOperation, models);
+ }
- public async Task MergeAsync(IEnumerable models) where T : new()
- {
- await this.StoreAsync(nStoreOperation.mergeOperation, models);
- }
+ public async Task MergeAsync(IEnumerable models) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.mergeOperation, models);
+ }
- public async Task InsertOrReplaceAsync(IEnumerable models) where T : new()
- {
- await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, models);
- }
-
- public async Task InsertOrReplaceAsync(T model) where T : new()
- {
- await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, new List() { model });
- }
-
- public async Task MergeOrInsertAsync(IEnumerable models) where T : new()
- {
- await this.StoreAsync(nStoreOperation.mergeOrInserOperation, models);
- }
-
- public async Task MergeOrInsertAsync(T model) where T : new()
- {
- await this.StoreAsync(nStoreOperation.mergeOrInserOperation, new List() { model });
- }
-
- public async Task QueryAsync(string partitionKey, string rowKey, int maxItems = 0) where T : new()
- {
- var result = await QueryAsyncInternal(partitionKey, rowKey, maxItems);
- return result.FirstOrDefault();
- }
-
- public async Task> QueryAsync(string partitionKey, int maxItems = 0) where T : new()
- {
- return await QueryAsyncInternal(partitionKey, null, maxItems);
- }
-
- public async Task> QueryAsync(int maxItems = 0) where T: new()
- {
- return await QueryAsyncInternal(null, null, maxItems);
- }
-
- private string GetTableName()
- {
- return GetTableName(typeof(T));
- }
-
- private string GetTableName(Type entityType)
- {
- // lookup the entitymapper
- var entityMapper = _entityMapperRegistry[entityType];
-
- // get the table name
- return entityMapper.TableName;
- }
-
- public async Task StoreAsync(nStoreOperation storaeOperationType, IEnumerable models) where T : new()
- {
- try
- {
- // notify delegate
- if (_delegate != null)
- _delegate.OnStoring(typeof(T), storaeOperationType);
-
- // Retrieve a reference to the table.
- CloudTable table = GetTableReference(GetTableName());
-
- // Create the batch operation.
- List batchOperations = new List();
-
- // Create the first batch
- var currentBatch = new TableBatchOperation();
- batchOperations.Add(currentBatch);
-
- // lookup the entitymapper
- var entityMapper = _entityMapperRegistry[typeof(T)];
-
- // define the modelcounter
- int modelCounter = 0;
-
- // Add all items
- foreach (var model in models)
- {
- switch (storaeOperationType)
- {
- case nStoreOperation.insertOperation:
- currentBatch.Insert(new DynamicTableEntity(model, entityMapper));
- break;
- case nStoreOperation.insertOrReplaceOperation:
- currentBatch.InsertOrReplace(new DynamicTableEntity(model, entityMapper));
- break;
- case nStoreOperation.mergeOperation:
- currentBatch.Merge(new DynamicTableEntity(model, entityMapper));
- break;
- case nStoreOperation.mergeOrInserOperation:
- currentBatch.InsertOrMerge(new DynamicTableEntity(model, entityMapper));
- break;
- case nStoreOperation.delete:
- currentBatch.Delete(new DynamicTableEntity(model, entityMapper));
- break;
- }
-
- modelCounter++;
-
- if (modelCounter % 100 == 0)
- {
- currentBatch = new TableBatchOperation();
- batchOperations.Add(currentBatch);
- }
- }
-
- // execute
- foreach (var createdBatch in batchOperations)
- {
- if (createdBatch.Count() > 0)
- {
- await table.ExecuteBatchAsync(createdBatch);
-
- // notify delegate
- if (_delegate != null)
- _delegate.OnStored(typeof(T), storaeOperationType, createdBatch.Count(), null);
- }
- }
- }
- catch (StorageException ex)
- {
- // check the exception
- if (!_autoCreateTable || !ex.Message.StartsWith("0:The table specified does not exist", StringComparison.CurrentCulture))
- {
- // notify delegate
- if (_delegate != null)
- _delegate.OnStored(typeof(T), storaeOperationType, 0, ex);
-
- throw ex;
- }
-
- // try to create the table
- await CreateTableAsync();
-
- // retry
- await StoreAsync(storaeOperationType, models);
- }
- }
-
- public async Task DeleteAsync(T model) where T: new()
- {
- await this.StoreAsync(nStoreOperation.delete, new List() { model });
- }
-
- public async Task DeleteAsync(IEnumerable models) where T: new()
- {
- await this.StoreAsync(nStoreOperation.delete, models);
- }
-
- internal async Task> QueryAsyncInternalSinglePage(string partitionKey, string rowKey, int maxItems = 0, TableContinuationToken continuationToken = null) where T : new()
- {
- try
- {
- // notify delegate
- if (_delegate != null)
- _delegate.OnQuerying(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null);
-
- // Retrieve a reference to the table.
- CloudTable table = GetTableReference(GetTableName());
-
- // lookup the entitymapper
- var entityMapper = _entityMapperRegistry[typeof(T)];
-
- // Construct the query to get all entries
- TableQuery> query = new TableQuery>();
-
- // add partitionkey if exists
- string partitionKeyFilter = null;
- if (partitionKey != null)
- partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey);
-
- // add row key if exists
- string rowKeyFilter = null;
- if (rowKey != null)
- rowKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, rowKey);
-
- // define the max query items
- if (maxItems > 0)
- query = query.Take(maxItems);
-
- // build the query filter
- if (partitionKey != null && rowKey != null)
- query = query.Where(TableQuery.CombineFilters(partitionKeyFilter, TableOperators.And, rowKeyFilter));
- else if (partitionKey != null && rowKey == null)
- query = query.Where(partitionKeyFilter);
- else if (partitionKey == null && rowKey != null)
- throw new Exception("PartitionKey must have a value");
-
- // execute the query
- var queryResult = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
-
- // map all to the original models
- List result = new List();
- foreach (DynamicTableEntity model in queryResult)
- result.Add(model.Model);
-
- // notify delegate
- if (_delegate != null)
- _delegate.OnQueryed(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null, null);
-
- // done
- return new QueryResult()
- {
- Items = result.AsQueryable(),
- NextToken = queryResult.ContinuationToken
- };
-
- } catch(Exception e) {
-
- // notify delegate
- if (_delegate != null)
- _delegate.OnQueryed(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null, e);
-
- // throw exception
- throw e;
- }
- }
-
- private async Task> QueryAsyncInternal(string partitionKey, string rowKey, int maxItems = 0, TableContinuationToken nextToken = null) where T : new()
- {
- // query the first page
- var result = await QueryAsyncInternalSinglePage(partitionKey, rowKey, maxItems, nextToken);
-
- // check if we have reached the max items
- if (maxItems > 0 && result.Items.Count() >= maxItems)
- return result.Items;
-
- if (result.NextToken != null)
- return result.Items.Concat(await this.QueryAsyncInternal(partitionKey, rowKey, maxItems, result.NextToken));
- else
- return result.Items;
- }
-
- private CloudTable GetTableReference(string tableName) {
-
- // create the table client
- var storageTableClient = _storageAccount.CreateCloudTableClient();
+ public async Task InsertOrReplaceAsync(IEnumerable models) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, models);
+ }
- // Create the table client.
- CloudTableClient tableClient = _storageAccount.CreateCloudTableClient();
+ public async Task InsertOrReplaceAsync(T model) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, new List() { model });
+ }
- // Retrieve a reference to the table.
- return tableClient.GetTableReference(tableName);
- }
-
-
- public StorageContextQueryCursor QueryPaged(string partitionKey, string rowKey, int maxItems = 0) where T : new()
- {
- return new StorageContextQueryCursor(this, partitionKey, rowKey, maxItems);
- }
- }
+ public async Task MergeOrInsertAsync(IEnumerable models) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.mergeOrInserOperation, models);
+ }
+
+ public async Task MergeOrInsertAsync(T model) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.mergeOrInserOperation, new List() { model });
+ }
+
+ public async Task QueryAsync(string partitionKey, string rowKey, int maxItems = 0) where T : new()
+ {
+ var result = await QueryAsyncInternal(partitionKey, rowKey, maxItems);
+ return result.FirstOrDefault();
+ }
+
+ public async Task> QueryAsync(string partitionKey, int maxItems = 0) where T : new()
+ {
+ return await QueryAsyncInternal(partitionKey, null, maxItems);
+ }
+
+ public async Task> QueryAsync(int maxItems = 0) where T : new()
+ {
+ return await QueryAsyncInternal(null, null, maxItems);
+ }
+
+
+ public async Task> QueryAsyncWithFilter(string filter, string partitionKey) where T : new()
+ {
+ return await QueryAsyncInternalWithFilter(partitionKey, filter, 0);
+ }
+ public async Task> QueryAsyncWithFilter(string filter) where T : new()
+ {
+ return await QueryAsyncInternalWithFilter(null, filter, 0);
+ }
+
+ private string GetTableName()
+ {
+ return GetTableName(typeof(T));
+ }
+
+ private string GetTableName(Type entityType)
+ {
+ // lookup the entitymapper
+ var entityMapper = _entityMapperRegistry[entityType];
+
+ // get the table name
+ return entityMapper.TableName;
+ }
+
+ public async Task StoreAsync(nStoreOperation storaeOperationType, IEnumerable models) where T : new()
+ {
+ try
+ {
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnStoring(typeof(T), storaeOperationType);
+
+ // Retrieve a reference to the table.
+ CloudTable table = GetTableReference(GetTableName());
+
+ // Create the batch operation.
+ List batchOperations = new List();
+
+ // Create the first batch
+ var currentBatch = new TableBatchOperation();
+ batchOperations.Add(currentBatch);
+
+ // lookup the entitymapper
+ var entityMapper = _entityMapperRegistry[typeof(T)];
+
+ // define the modelcounter
+ int modelCounter = 0;
+
+ // Add all items
+ foreach (var model in models)
+ {
+ switch (storaeOperationType)
+ {
+ case nStoreOperation.insertOperation:
+ currentBatch.Insert(new DynamicTableEntity(model, entityMapper));
+ break;
+ case nStoreOperation.insertOrReplaceOperation:
+ currentBatch.InsertOrReplace(new DynamicTableEntity(model, entityMapper));
+ break;
+ case nStoreOperation.mergeOperation:
+ currentBatch.Merge(new DynamicTableEntity(model, entityMapper));
+ break;
+ case nStoreOperation.mergeOrInserOperation:
+ currentBatch.InsertOrMerge(new DynamicTableEntity(model, entityMapper));
+ break;
+ case nStoreOperation.delete:
+ currentBatch.Delete(new DynamicTableEntity(model, entityMapper));
+ break;
+ }
+
+ modelCounter++;
+
+ if (modelCounter % 100 == 0)
+ {
+ currentBatch = new TableBatchOperation();
+ batchOperations.Add(currentBatch);
+ }
+ }
+
+ // execute
+ foreach (var createdBatch in batchOperations)
+ {
+ if (createdBatch.Count() > 0)
+ {
+ await table.ExecuteBatchAsync(createdBatch);
+
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnStored(typeof(T), storaeOperationType, createdBatch.Count(), null);
+ }
+ }
+ }
+ catch (StorageException ex)
+ {
+ // check the exception
+ if (!_autoCreateTable || !ex.Message.StartsWith("0:The table specified does not exist", StringComparison.CurrentCulture))
+ {
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnStored(typeof(T), storaeOperationType, 0, ex);
+
+ throw ex;
+ }
+
+ // try to create the table
+ await CreateTableAsync();
+
+ // retry
+ await StoreAsync(storaeOperationType, models);
+ }
+ }
+
+ public async Task DeleteAsync(T model) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.delete, new List() { model });
+ }
+
+ public async Task DeleteAsync(IEnumerable models) where T : new()
+ {
+ await this.StoreAsync(nStoreOperation.delete, models);
+ }
+
+ internal async Task> QueryAsyncInternalSinglePage(string partitionKey, string rowKey, int maxItems = 0, TableContinuationToken continuationToken = null, string filter = null) where T : new()
+ {
+ try
+ {
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnQuerying(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null);
+
+ // exit early if partitionKey is unspecified
+ if (partitionKey == null && rowKey != null)
+ throw new Exception("PartitionKey must have a value if RowKey is specified");
+
+ // Retrieve a reference to the table.
+ CloudTable table = GetTableReference(GetTableName());
+
+ // lookup the entitymapper
+ var entityMapper = _entityMapperRegistry[typeof(T)];
+
+ // Construct the query to get all entries
+ TableQuery> query = new TableQuery>();
+
+ // add partitionkey if exists
+ string partitionKeyFilter = null;
+ if (partitionKey != null)
+ partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey);
+
+ // add row key if exists
+ string rowKeyFilter = null;
+ if (rowKey != null)
+ rowKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, rowKey);
+
+ // define the max query items
+ if (maxItems > 0)
+ query = query.Take(maxItems);
+
+ string where = partitionKeyFilter;
+ // build the query filter
+ if (rowKey != null)
+ where = TableQueryEx.CheckAndCombineFilters(where, TableOperators.And, rowKeyFilter);
+ if (filter != null)
+ where = TableQueryEx.CheckAndCombineFilters(where, TableOperators.And, filter);
+
+
+ query = query.Where(where);
+
+ // execute the query
+ var queryResult = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+ // map all to the original models
+ var result = new List();
+ var relatedItems = new List>();
+
+ foreach (DynamicTableEntity model in queryResult)
+ {
+ // find associate items in related tables
+ var relatedItem = LoadRelatedTables(model);
+ if (relatedItem != null)
+ relatedItems.Add(relatedItem);
+
+ result.Add(model.Model);
+ }
+
+ // load related items
+ await LoadEagerRelatedItems(relatedItems);
+
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnQueryed(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null, null);
+
+ // done
+ return new QueryResult()
+ {
+ Items = result.AsQueryable(),
+ NextToken = queryResult.ContinuationToken
+ };
+
+ }
+ catch (Exception e)
+ {
+
+ // notify delegate
+ if (_delegate != null)
+ _delegate.OnQueryed(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null, e);
+
+ // throw exception
+ throw e;
+ }
+ }
+
+
+ private async Task> QueryAsyncInternal(string partitionKey, string rowKey, int maxItems = 0, TableContinuationToken nextToken = null) where T : new()
+ {
+ // query the first page
+ var result = await QueryAsyncInternalSinglePage(partitionKey, rowKey, maxItems, nextToken);
+
+ // check if we have reached the max items
+ if (maxItems > 0 && result.Items.Count() >= maxItems)
+ return result.Items;
+
+ if (result.NextToken != null)
+ return result.Items.Concat(await this.QueryAsyncInternal(partitionKey, rowKey, maxItems, result.NextToken));
+ else
+ return result.Items;
+ }
+ private async Task> QueryAsyncInternalWithFilter(string partitionKey, string filter, int maxItems = 0, TableContinuationToken nextToken = null) where T : new()
+ {
+ // query the first page
+ var result = await QueryAsyncInternalSinglePage(partitionKey, null, maxItems, nextToken, filter);
+
+ // check if we have reached the max items
+ if (maxItems > 0 && result.Items.Count() >= maxItems)
+ return result.Items;
+
+ if (result.NextToken != null)
+ return result.Items.Concat(await this.QueryAsyncInternalWithFilter(partitionKey, filter, maxItems, result.NextToken));
+ else
+ return result.Items;
+ }
+
+ private CloudTable GetTableReference(string tableName)
+ {
+
+ // create the table client
+ var storageTableClient = _storageAccount.CreateCloudTableClient();
+
+ // Create the table client.
+ CloudTableClient tableClient = _storageAccount.CreateCloudTableClient();
+
+ // Retrieve a reference to the table.
+ return tableClient.GetTableReference(tableName);
+ }
+
+
+ public StorageContextQueryCursor QueryPaged(string partitionKey, string rowKey, int maxItems = 0) where T : new()
+ {
+ return new StorageContextQueryCursor(this, partitionKey, rowKey, maxItems);
+ }
+
+ private RelatedTableItem LoadRelatedTables(DynamicTableEntity model) where T : new()
+ {
+ IEnumerable objectProperties = model.Model.GetType().GetTypeInfo().GetProperties();
+
+ foreach (PropertyInfo property in objectProperties)
+ {
+ if (property.GetCustomAttribute() != null)
+ {
+ var relatedTable = property.GetCustomAttribute();
+
+ Type endType;
+ if (property.PropertyType.IsGenericOfType(typeof(Lazy<>)))
+ {
+ endType = property.PropertyType.GetTypeInfo().GenericTypeArguments[0];
+ }
+ else
+ {
+ endType = property.PropertyType;
+ }
+
+ // determine the partition key
+ string extPartition = relatedTable.PartitionKey;
+ if (!string.IsNullOrWhiteSpace(extPartition))
+ {
+ // if the partition key is the name of a property on the model, get the value
+ var partitionProperty = objectProperties.Where((pi) => pi.Name == relatedTable.PartitionKey).FirstOrDefault();
+ if (partitionProperty != null)
+ {
+ extPartition = partitionProperty.GetValue(model.Model).ToString();
+ }
+ }
+
+ string extRowKey = relatedTable.RowKey;
+ if (!string.IsNullOrWhiteSpace(extRowKey))
+ {
+ // if the row key is the name of a property on the model, get the value
+ var rowkeyProperty = objectProperties.Where((pi) => pi.Name == relatedTable.PartitionKey).FirstOrDefault();
+ if (rowkeyProperty != null)
+ {
+ extRowKey = rowkeyProperty.GetValue(model.Model).ToString();
+ }
+ }
+ else
+ {
+ // if the type of the object is the name of a property on the model, get the value of that property as the rowkey
+ var rowkeyProperty = objectProperties.Where((pi) => pi.Name == endType.Name).FirstOrDefault();
+ if (rowkeyProperty != null)
+ {
+ extRowKey = rowkeyProperty.GetValue(model.Model).ToString();
+ }
+ }
+
+ // make a dynamic reference to the query method
+ var method = typeof(StorageContext).GetMethod(nameof(QueryAsync), new[] { typeof(string), typeof(string), typeof(int) });
+ var generic = method.MakeGenericMethod(endType);
+
+ // if the property is a lazy type, create the lazy initialization
+ if (property.PropertyType.IsGenericOfType(typeof(Lazy<>)))
+ {
+ var lazyType = typeof(DynamicLazy<>);
+ var constructed = lazyType.MakeGenericType(endType);
+
+ object o = Activator.CreateInstance(constructed, new Func(() =>
+ {
+ var waitable = (dynamic)generic.Invoke(this, new object[] { extPartition, extRowKey, 1 });
+ var r = waitable.Result;
+
+ return r;
+ }));
+ property.SetValue(model.Model, o);
+
+ }
+ else
+ {
+ // return a related table item, in order to optimize the eager loading
+ return new RelatedTableItem()
+ {
+ RowKey = extRowKey,
+ PartitionKey = extPartition,
+ Model = model,
+ Property = property
+ };
+ }
+ }
+ }
+ return null;
+ }
+
+ private async Task LoadEagerRelatedItems(List> relatedItems) where T : new()
+ {
+
+ var method = typeof(StorageContext).GetMethod(nameof(QueryAsyncWithFilter), new[] { typeof(string), typeof(string) });
+
+ //group by the property type (same table)
+ var relatedItemPropertyGroups = relatedItems.GroupBy((i) => i.Property.PropertyType);
+ foreach (var relatedItemPropertyGroup in relatedItemPropertyGroups)
+ {
+ // group by partition key for better query performance
+ var relatedItemGroups = relatedItemPropertyGroup.GroupBy((i) => i.PartitionKey);
+ var propertyType = relatedItemPropertyGroup.Key;
+ var generic = method.MakeGenericMethod(propertyType);
+
+ // lookup the entitymapper
+ var entityMapper = _entityMapperRegistry[propertyType];
+
+ foreach (var relatedItemGroup in relatedItemGroups)
+ {
+ // create a rowkey filter
+ var rowKeysFilter = TableQueryEx.CombineFilters(
+ relatedItemGroup.Select((i) => TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, i.RowKey)),
+ TableOperators.Or
+ );
+
+ // query dynamically
+ var task = (Task)generic.Invoke(this, new object[] { rowKeysFilter, relatedItemGroup.Key });
+ await task;
+
+ var r = (System.Collections.IEnumerable)((dynamic)task).Result;
+ foreach (var item in r)
+ {
+ string rowKey = "";
+
+ // get the rowkey
+ // we have the partition key because we have grouped by it before
+ if (entityMapper.RowKeyFormat.Contains("{{") && entityMapper.RowKeyFormat.Contains("}}"))
+ {
+ var template = Handlebars.Compile(entityMapper.RowKeyFormat);
+ rowKey= template(item);
+ }
+ else
+ {
+ var propertyInfo = item.GetType().GetRuntimeProperty(entityMapper.RowKeyFormat);
+ rowKey = propertyInfo.GetValue(item) as String;
+ }
+
+
+ // find a set models which reference the item (the same item can be referenced from multiple models)
+ var models = relatedItemGroup.Where((i) => i.RowKey == rowKey && i.PartitionKey == relatedItemGroup.Key);
+ foreach (var model in models)
+ {
+ model.Property.SetValue(model.Model.Model, item);
+ }
+
+ }
+ }
+ }
+ }
+ }
}
diff --git a/README.md b/README.md
index 882a64d..fb6215c 100644
--- a/README.md
+++ b/README.md
@@ -142,3 +142,57 @@ public class JObjectModel
public Dictionary Data { get; set; } = new Dictionary();
}
```
+
+## Related tables
+It is possible to automatically load related tables, either lazily or eagerly. In order to load lazily simply pack the object in the `Lazy<>` type.
+
+```csharp
+[Storable(Tablename: "JObjectModel")]
+public class Model
+{
+ [PartitionKey]
+ [RowKey]
+ public string UUID { get; set; }
+
+ public string UserId {get; set; }
+
+ //This is the rowkey of the OtherModel
+ public string OtherModel { get; set; }
+
+ //Partition key must be specified explicitly, rowkey defaults to the name of the type (here: OtherModel)
+ [RelatedTable("UserId")]
+ public Lazy OtherModelObject { get; set; }
+}
+```
+It is possible to specify the rowkey explicitly:
+```csharp
+[Storable(Tablename: "JObjectModel")]
+public class Model
+{
+ [PartitionKey]
+ [RowKey]
+ public string UUID { get; set; }
+
+ public string UserId {get; set; }
+
+ public string OtherModelId { get; set; }
+
+ [RelatedTable("UserId", RowKey="OtherModelId")]
+ public OtherModel OtherModel { get; set; }
+}
+```
+If neither the rowkey or the partition key is the name of a property of the object they are used directly as strings, and obviously to reduce the possible causes of errors it is recommended to use the `nameof`:
+```csharp
+[Storable(Tablename: "Models")]
+public class Model
+{
+ [PartitionKey]
+ [RowKey]
+ public string UUID { get; set; }
+
+ public string OtherModelId { get; set; }
+
+ [RelatedTable(nameof(UUID), RowKey=nameof(OtherModelId))]
+ public Lazy OtherModel { get; set; }
+}
+```
\ No newline at end of file