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