From 7ff946621081b8c8cdaa123217c789b239fc2e51 Mon Sep 17 00:00:00 2001 From: Dirk Eisenberg Date: Tue, 30 Aug 2022 00:22:02 +0200 Subject: [PATCH] Added entity writer based on new azure sdk --- .../ITS001StoreWithStaticEntityMapper.cs | 28 +- .../Attributes/IVirtualTypeAttribute.cs | 12 + .../Attributes/StoreAsJsonObjectAttribute.cs | 21 +- .../Attributes/VirtualListAttribute.cs | 30 +- .../Attributes/VirtualTypeAttribute.cs | 3 +- ...eHelpers.WindowsAzure.Storage.Table.csproj | 8 +- .../DynamicTableEntityNew.cs | 287 ++++++++++++++++++ .../Extensions/TableClientExtensions.cs | 38 +++ .../Serialization/TableEntityBuilder.cs | 36 +++ .../Serialization/TableEntityDynamic.cs | 99 ++++++ .../StorageContext.cs | 208 +++++-------- 11 files changed, 635 insertions(+), 135 deletions(-) create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Attributes/IVirtualTypeAttribute.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntityNew.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableClientExtensions.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityBuilder.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs index 4731eaa..899888b 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs @@ -19,6 +19,24 @@ public ITS001StoreWithStaticEntityMapper(ITestEnvironment env) } [Fact] + public async Task VerifyTableExists() + { + using (var scp = new StorageContext(env.ConnectionString)) + { + // set the tablename context + scp.SetTableContext(); + + // configure the entity mapper + scp.AddEntityMapper(typeof(UserModel), new DynamicTableEntityMapper() { TableName = "UserProfiles", PartitionKeyFormat = "Contact", RowKeyFormat = "Contact" }); + + Assert.False(await scp.ExistsTableAsync()); + + await scp.CreateTableAsync(); + Assert.True(await scp.ExistsTableAsync()); + } + } + + [Fact] public async Task VerifyStaticEntityMapperOperations() { using (var scp = new StorageContext(env.ConnectionString)) @@ -41,9 +59,13 @@ public async Task VerifyStaticEntityMapperOperations() { // ensure the table exists await sc.CreateTableAsync(); - - // inser the model - await sc.MergeOrInsertAsync(user); + + // ensure we are empty + var resultEmpty = await scp.QueryAsync(); + Assert.Empty(resultEmpty); + + // inser the model + await sc.MergeOrInsertAsync(user); } // verify if the model was created diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/IVirtualTypeAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/IVirtualTypeAttribute.cs new file mode 100644 index 0000000..413a34a --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/IVirtualTypeAttribute.cs @@ -0,0 +1,12 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Serialization; +using System.Reflection; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes +{ + public interface IVirtualTypeAttribute + { + void WriteProperty(PropertyInfo propertyInfo, T obj, TableEntityBuilder builder); + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StoreAsJsonObjectAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StoreAsJsonObjectAttribute.cs index 71a5d98..225ac40 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StoreAsJsonObjectAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StoreAsJsonObjectAttribute.cs @@ -1,14 +1,15 @@ using System; using System.Collections; using System.Reflection; +using CoreHelpers.WindowsAzure.Storage.Table.Serialization; using Microsoft.WindowsAzure.Storage.Table; using Newtonsoft.Json; namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes { [AttributeUsage(AttributeTargets.Property)] - public class StoreAsJsonObjectAttribute : StoreAsAttribute - { + public class StoreAsJsonObjectAttribute : StoreAsAttribute, IVirtualTypeAttribute + { protected Type ObjectType { get; set; } public StoreAsJsonObjectAttribute() @@ -54,5 +55,19 @@ public override Object ConvertFromEntityProperty(PropertyInfo property, EntityPr return JsonConvert.DeserializeObject(entityProperty.StringValue, property.PropertyType); } } - } + + public void WriteProperty(PropertyInfo propertyInfo, T obj, TableEntityBuilder builder) + { + // get the value + var element = propertyInfo.GetValue(obj); + if (element == null) + return; + + // convert to strong + var stringifiedElement = JsonConvert.SerializeObject(element); + + // add the property + builder.AddProperty(propertyInfo.Name, stringifiedElement); + } + } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualListAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualListAttribute.cs index 6d21b0c..f5fac10 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualListAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualListAttribute.cs @@ -5,12 +5,13 @@ using HandlebarsDotNet; using Microsoft.WindowsAzure.Storage.Table; using CoreHelpers.WindowsAzure.Storage.Table.Extensions; +using CoreHelpers.WindowsAzure.Storage.Table.Serialization; namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes { [AttributeUsage(AttributeTargets.Property)] - public class VirtualListAttribute : VirtualTypeAttribute - { + public class VirtualListAttribute : VirtualTypeAttribute, IVirtualTypeAttribute + { private HandlebarsTemplate TemplateFunction { get; set; } private string DigitFormat { get; set; } @@ -73,5 +74,28 @@ public override void ReadProperty(PropertyInfo propertyInfo, object obj, IDictio } } - } + + public void WriteProperty(PropertyInfo propertyInfo, T obj, TableEntityBuilder builder) + { + // get the value + var arrayValue = propertyInfo.GetValue(obj); + + // check if enumerable + if ((arrayValue as IList) == null) + return; + + // visit every element + for (int idx = 0; idx < (arrayValue as IList).Count; idx++) + { + // get the element + var element = (arrayValue as IList)[idx]; + + // generate the property name + var propertyName = TemplateFunction(new { index = idx.ToString(DigitFormat) }); + + // write the property + builder.AddProperty(propertyName, element); + } + } + } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualTypeAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualTypeAttribute.cs index 6a60504..826e935 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualTypeAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/VirtualTypeAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using CoreHelpers.WindowsAzure.Storage.Table.Serialization; using Microsoft.WindowsAzure.Storage.Table; namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes @@ -9,6 +10,6 @@ public abstract class VirtualTypeAttribute : Attribute { public abstract void WriteProperty(PropertyInfo propertyInfo, Object obj, Dictionary targetList); - public abstract void ReadProperty(PropertyInfo propertyInfo, Object obj, IDictionary entityProperties); + public abstract void ReadProperty(PropertyInfo propertyInfo, Object obj, IDictionary entityProperties); } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj index daac95e..2dafc24 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj +++ b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj @@ -18,7 +18,11 @@ - + + + + + @@ -26,6 +30,7 @@ + @@ -33,6 +38,7 @@ + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntityNew.cs b/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntityNew.cs new file mode 100644 index 0000000..e1f22a6 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/DynamicTableEntityNew.cs @@ -0,0 +1,287 @@ +using System; +using System.Reflection; +using System.Collections.Generic; +using HandlebarsDotNet; +using Azure.Data.Tables; +using Azure; +using ITableEntity = Azure.Data.Tables.ITableEntity; +using TableEntity = Azure.Data.Tables.TableEntity; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; +using System.Runtime.Serialization; +using CoreHelpers.WindowsAzure.Storage.Table.Extensions; +using System.Collections; + +namespace CoreHelpers.WindowsAzure.Storage.Table +{ + internal class DynamicTableEntityNew : ITableEntity, IDictionary where T : new() + { + private T _srcModel { get; set; } + private DynamicTableEntityMapper _entityMapper { get; set; } + + public DynamicTableEntityNew() + { + _srcModel = new T(); + BuildPropertyInfoMap(); + } + + public DynamicTableEntityNew(T src, DynamicTableEntityMapper entityMapper) + { + _srcModel = src; + _entityMapper = entityMapper; + ETag = new ETag("*"); + BuildPropertyInfoMap(); + } + + public ETag ETag { get; set; } + + + public string PartitionKey + { + get { return GetTableStorageDefaultProperty(_entityMapper.PartitionKeyFormat); } + set { SetTableStorageDefaultProperty(value); } + } + + public string RowKey + { + get { return GetTableStorageDefaultProperty(_entityMapper.RowKeyFormat); } + set { SetTableStorageDefaultProperty(value); } + } + + public DateTimeOffset? Timestamp { get; set; } + + public ICollection Keys + { + get + { + return _propertyInfoKeys.Keys; + } + } + + public ICollection Values + { + get + { + throw new NotImplementedException(); + } + } + + public int Count + { + get + { + throw new NotImplementedException(); + } + } + + public bool IsReadOnly + { + get + { + throw new NotImplementedException(); + } + } + + public object this[string key] + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + private S GetTableStorageDefaultProperty(string format) where S : class + { + if (typeof(S) == typeof(string) && format.Contains("{{") && format.Contains("}}")) + { + var template = Handlebars.Compile(format); + return template(_srcModel) as S; + } + else + { + var propertyInfo = _srcModel.GetType().GetRuntimeProperty(format); + return propertyInfo.GetValue(_srcModel) as S; + } + } + + private void SetTableStorageDefaultProperty(S value) where A : Attribute + { + foreach (var property in _srcModel.GetType()?.GetRuntimeProperties()) + { + if (property.GetCustomAttribute() != null && property?.GetCustomAttribute() != null) + { + var setter = property.GetSetMethod(); + if (setter != null && !setter.IsStatic) + property.SetValue(_srcModel, value); + } + } + } + + internal static bool ShouldSkipProperty(PropertyInfo property) + { + // reserved properties + string propName = property.Name; + if (propName == TableConstants.PartitionKey || + propName == TableConstants.RowKey || + propName == TableConstants.Timestamp || + propName == TableConstants.Etag) + { + return true; + } + + MethodInfo setter = property.SetMethod; + MethodInfo getter = property.GetMethod; + + // Enforce public getter / setter + if (setter == null || !setter.IsPublic || getter == null || !getter.IsPublic) + { + // Logger.LogInformational(operationContext, SR.TraceNonPublicGetSet, property.Name); + return true; + } + + // Skip static properties + if (setter.IsStatic) + { + return true; + } + + // properties with [IgnoreDataMember] + if (property.GetCustomAttribute(typeof(IgnoreDataMemberAttribute)) != null) + { + // Logger.LogInformational(operationContext, SR.TraceIgnoreAttribute, property.Name); + return true; + } + + return false; + } + + private Dictionary _propertyInfoKeys = new Dictionary(); + + private void BuildPropertyInfoMap() + { + _propertyInfoKeys = new Dictionary(); + + IEnumerable objectProperties = _srcModel.GetType().GetTypeInfo().GetProperties(); + + foreach (PropertyInfo property in objectProperties) + { + if (ShouldSkipProperty(property)) + { + continue; + } + + _propertyInfoKeys.Add(property.Name, property); + } + } + + public void Add(string key, object value) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(string key) + { + return _propertyInfoKeys.ContainsKey(key); + } + + public bool Remove(string key) + { + // never happens + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, out object value) + { + throw new NotImplementedException(); + } + + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + // never happens + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + return new DynamicTableEntityNewEnumerator(_srcModel, _propertyInfoKeys); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } + + + internal class DynamicTableEntityNewEnumerator : IEnumerator> where T: new() + { + private Dictionary _propertyMap; + private IEnumerator> _propertyMapEnumerator; + + private T _model; + + + public DynamicTableEntityNewEnumerator(T model, Dictionary propertyMap) + { + _propertyMap = propertyMap; + _model = model; + _propertyMapEnumerator = _propertyMap.GetEnumerator(); + } + + public KeyValuePair Current { + get { + return new KeyValuePair( + _propertyMapEnumerator.Current.Key, + _propertyMapEnumerator.Current.Value.GetValue(_model) + ); + } + } + + object IEnumerator.Current + { + get + { + return this.Current; + } + } + + public void Dispose() + { + _propertyMapEnumerator.Dispose(); + } + + public bool MoveNext() + { + return _propertyMapEnumerator.MoveNext(); + } + + public void Reset() + { + _propertyMapEnumerator.Reset(); + } + } +} diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableClientExtensions.cs b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableClientExtensions.cs new file mode 100644 index 0000000..eb67a85 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TableClientExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using Azure.Data.Tables; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Extensions +{ + public static class TableClientExtensions + { + public static async Task DeleteIfExistsAsync(this TableClient tc) + { + try + { + await tc.DeleteAsync(); + } catch (Exception) + {} + } + + public static async Task ExistsAsync(this TableClient tc) + { + try + { + await tc.GetAccessPoliciesAsync(); + return true; + } catch(Azure.RequestFailedException ex) + { + if (ex.Status == 404) + return false; + else + { + ExceptionDispatchInfo.Capture(ex).Throw(); + return false; + } + } + } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityBuilder.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityBuilder.cs new file mode 100644 index 0000000..48a5d67 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Azure.Data.Tables; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Serialization +{ + public class TableEntityBuilder + { + private IDictionary _data = new Dictionary(); + + + public TableEntityBuilder AddPartitionKey(string pkey) + { + _data.Add("PartitionKey", pkey); + return this; + } + + public TableEntityBuilder AddRowKey(string rkey) + { + _data.Add("RowKey", rkey); + return this; + } + + public TableEntityBuilder AddProperty(string property, object value) + { + _data.Add(property, value); + return this; + } + + public TableEntity Build() + { + return new TableEntity(_data); + } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs new file mode 100644 index 0000000..baf4739 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using Azure.Data.Tables; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; +using HandlebarsDotNet; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Serialization +{ + public static class TableEntityDynamic + { + public static TableEntity ToEntity(T model, DynamicTableEntityMapper entityMapper) where T: new() + { + var builder = new TableEntityBuilder(); + + // set the keys + builder.AddPartitionKey(GetTableStorageDefaultProperty(entityMapper.PartitionKeyFormat, model)); + builder.AddRowKey(GetTableStorageDefaultProperty(entityMapper.RowKeyFormat, model)); + + // get all properties from model + IEnumerable objectProperties = model.GetType().GetTypeInfo().GetProperties(); + + // visit all properties + foreach (PropertyInfo property in objectProperties) + { + if (ShouldSkipProperty(property)) + continue; + + // check if we have a special convert attached via attribute if so generate the required target + // properties with the correct converter + var virtualTypeAttribute = property.GetCustomAttributes().Where(a => a is IVirtualTypeAttribute).Select(a => a as IVirtualTypeAttribute).FirstOrDefault(); + if (virtualTypeAttribute != null) + virtualTypeAttribute.WriteProperty(property, model, builder); + else + builder.AddProperty(property.Name, property.GetValue(model, null)); + } + + // build the result + return builder.Build(); + } + + private static S GetTableStorageDefaultProperty(string format, T model) where S : class + { + if (typeof(S) == typeof(string) && format.Contains("{{") && format.Contains("}}")) + { + var template = Handlebars.Compile(format); + return template(model) as S; + } + else + { + var propertyInfo = model.GetType().GetRuntimeProperty(format); + return propertyInfo.GetValue(model) as S; + } + } + + + private static bool ShouldSkipProperty(PropertyInfo property) + { + // reserved properties + string propName = property.Name; + if (propName == TableConstants.PartitionKey || + propName == TableConstants.RowKey || + propName == TableConstants.Timestamp || + propName == TableConstants.Etag) + { + return true; + } + + + MethodInfo setter = property.SetMethod; + MethodInfo getter = property.GetMethod; + + // Enforce public getter / setter + if (setter == null || !setter.IsPublic || getter == null || !getter.IsPublic) + { + // Logger.LogInformational(operationContext, SR.TraceNonPublicGetSet, property.Name); + return true; + } + + // Skip static properties + if (setter.IsStatic) + { + return true; + } + + // properties with [IgnoreAttribute] + if (property.GetCustomAttribute(typeof(IgnoreDataMemberAttribute)) != null) + { + // Logger.LogInformational(operationContext, SR.TraceIgnoreAttribute, property.Name); + return true; + } + + return false; + } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs index 2af3477..129a1df 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContext.cs @@ -12,6 +12,8 @@ using CoreHelpers.WindowsAzure.Storage.Table.Services; using System.Runtime.ExceptionServices; using System.Text.RegularExpressions; +using Azure.Data.Tables; +using CoreHelpers.WindowsAzure.Storage.Table.Serialization; namespace CoreHelpers.WindowsAzure.Storage.Table { @@ -36,19 +38,21 @@ public class StorageContext : IStorageContext private bool _autoCreateTable { get; set; } = false; private IStorageContextDelegate _delegate { get; set; } private string _tableNamePrefix; + private string _connectionString; public StorageContext(string storageAccountName, string storageAccountKey, string storageEndpointSuffix = null) { - var connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2}", "https", storageAccountName, storageAccountKey); + _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); + _connectionString = String.Format("DefaultEndpointsProtocol={0};AccountName={1};AccountKey={2};EndpointSuffix={3}", "https", storageAccountName, storageAccountKey, storageEndpointSuffix); - _storageAccount = CloudStorageAccount.Parse(connectionString); + _storageAccount = CloudStorageAccount.Parse(_connectionString); } public StorageContext(string connectionString) { - _storageAccount = CloudStorageAccount.Parse(connectionString); + _connectionString = connectionString; + _storageAccount = CloudStorageAccount.Parse(_connectionString); } public StorageContext(StorageContext parentContext) @@ -64,6 +68,9 @@ public StorageContext(StorageContext parentContext) // take the tablename prefix _tableNamePrefix = parentContext._tableNamePrefix; + + // store the connection string + _connectionString = parentContext._connectionString; } public void Dispose() @@ -101,11 +108,7 @@ public void RemoveEntityMapper(Type entityType) public void AddAttributeMapper() { AddAttributeMapper(Assembly.GetEntryAssembly()); - AddAttributeMapper(Assembly.GetCallingAssembly()); - - /*foreach(var assembly in Assembly.GetEntryAssembly().GetReferencedAssemblies()) { - AddAttributeMapper(assembly); - } */ + AddAttributeMapper(Assembly.GetCallingAssembly()); } internal void AddAttributeMapper(Assembly assembly) @@ -199,126 +202,79 @@ public void OverrideTableName(Type entityType, string tableName) _entityMapperRegistry[entityType] = duplicatedMapper; } } - - public Task CreateTableAsync(Type entityType, bool ignoreErrorIfExists = true) + + public async Task ExistsTableAsync() + { + var tc = GetTableClient(GetTableName(typeof(T))); + return await tc.ExistsAsync(); + } + + public async Task CreateTableAsync(Type entityType, bool ignoreErrorIfExists = true) { - // Retrieve a reference to the table. - CloudTable table = GetTableReference(GetTableName(entityType)); + var tc = GetTableClient(GetTableName(entityType)); if (ignoreErrorIfExists) - { - // Create the table if it doesn't exist. - return table.CreateIfNotExistsAsync(); - } + await tc.CreateIfNotExistsAsync(); else - { - // Create table and throw error - return table.CreateAsync(); - } + await tc.CreateAsync(); } - - + public Task CreateTableAsync(bool ignoreErrorIfExists = true) - { - return CreateTableAsync(typeof(T), ignoreErrorIfExists); - } + => CreateTableAsync(typeof(T), ignoreErrorIfExists); public void CreateTable(bool ignoreErrorIfExists = true) - { - this.CreateTableAsync(ignoreErrorIfExists).GetAwaiter().GetResult(); - } + => 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)); - + var tc = GetTableClient(GetTableName(entityType)); if (ignoreErrorIfNotExists) - await table.DeleteIfExistsAsync(); + await tc.DeleteIfExistsAsync(); else - await table.DeleteAsync(); - } - - public async Task ExistsTableAsync() - { - CloudTable table = GetTableReference(GetTableName(typeof(T))); - return await table.ExistsAsync(); + await tc.DeleteAsync(); } public async Task DropTableAsync(bool ignoreErrorIfNotExists = true) - { - await DropTableAsync(typeof(T), ignoreErrorIfNotExists); - } + => await DropTableAsync(typeof(T), ignoreErrorIfNotExists); - public void DropTable(bool ignoreErrorIfNotExists = true) - { - Task.Run(async () => await DropTableAsync(typeof(T), ignoreErrorIfNotExists)).Wait(); - } + 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); - } + => await this.StoreAsync(nStoreOperation.insertOperation, models); public async Task MergeAsync(IEnumerable models) where T : new() - { - await this.StoreAsync(nStoreOperation.mergeOperation, models); - } + => await this.StoreAsync(nStoreOperation.mergeOperation, models); public async Task InsertOrReplaceAsync(IEnumerable models) where T : new() - { - await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, models); - } + => await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, models); public async Task InsertOrReplaceAsync(T model) where T : new() - { - await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, new List() { model }); - } + => await this.StoreAsync(nStoreOperation.insertOrReplaceOperation, new List() { model }); public async Task MergeOrInsertAsync(IEnumerable models) where T : new() - { - await this.StoreAsync(nStoreOperation.mergeOrInserOperation, models); - } + => await this.StoreAsync(nStoreOperation.mergeOrInserOperation, models); public async Task MergeOrInsertAsync(T model) where T : new() - { - await this.StoreAsync(nStoreOperation.mergeOrInserOperation, new List() { model }); - } + => 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, null, maxItems); - return result.FirstOrDefault(); - } + => (await QueryAsyncInternal(partitionKey, rowKey, null, maxItems)).FirstOrDefault(); public async Task> QueryAsync(string partitionKey, IEnumerable queryFilters, int maxItems = 0) where T : new() - { - return await QueryAsyncInternal(partitionKey, null, queryFilters, maxItems); - } + => await QueryAsyncInternal(partitionKey, null, queryFilters, maxItems); public async Task> QueryAsync(string partitionKey, int maxItems = 0) where T : new() - { - return await QueryAsyncInternal(partitionKey, null, null, maxItems); - } + => await QueryAsyncInternal(partitionKey, null, null, maxItems); - public async Task> QueryAsync(int maxItems = 0) where T: new() - { - return await QueryAsyncInternal(null, null, null, maxItems); - } + public async Task> QueryAsync(int maxItems = 0) where T: new() + => await QueryAsyncInternal(null, null, null, maxItems); - private string GetTableName() - { - return GetTableName(typeof(T)); - } + private string GetTableName() + => GetTableName(typeof(T)); private string GetTableName(Type entityType) - { - // lookup the entitymapper - var entityMapper = _entityMapperRegistry[entityType]; - - // get the table name - return GetTableName(entityMapper.TableName); - } + => GetTableName(_entityMapperRegistry[entityType].TableName); private string GetTableName(string tableName) { @@ -336,19 +292,20 @@ private string GetTableName(string tableName) // 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)]; + // Retrieve a reference to the table. + var tc = GetTableClient(GetTableName()); + + // Create the batch + var tableTransactionsBatch = new List>(); + + // Create the frist transaction + var tableTransactions = new List(); + tableTransactionsBatch.Add(tableTransactions); + + // lookup the entitymapper + var entityMapper = _entityMapperRegistry[typeof(T)]; // define the modelcounter int modelCounter = 0; @@ -359,37 +316,37 @@ private string GetTableName(string tableName) switch (storaeOperationType) { case nStoreOperation.insertOperation: - currentBatch.Insert(new DynamicTableEntity(model, entityMapper)); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Add, TableEntityDynamic.ToEntity(model, entityMapper))); break; case nStoreOperation.insertOrReplaceOperation: - currentBatch.InsertOrReplace(new DynamicTableEntity(model, entityMapper)); - break; + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertReplace, TableEntityDynamic.ToEntity(model, entityMapper))); + break; case nStoreOperation.mergeOperation: - currentBatch.Merge(new DynamicTableEntity(model, entityMapper)); - break; + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpdateMerge, TableEntityDynamic.ToEntity(model, entityMapper))); + break; case nStoreOperation.mergeOrInserOperation: - currentBatch.InsertOrMerge(new DynamicTableEntity(model, entityMapper)); - break; - case nStoreOperation.delete: - currentBatch.Delete(new DynamicTableEntity(model, entityMapper)); - break; + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertMerge, TableEntityDynamic.ToEntity(model, entityMapper))); + break; + case nStoreOperation.delete: + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Delete, TableEntityDynamic.ToEntity(model, entityMapper))); + break; } modelCounter++; if (modelCounter % 100 == 0) { - currentBatch = new TableBatchOperation(); - batchOperations.Add(currentBatch); + tableTransactions = new List(); + tableTransactionsBatch.Add(tableTransactions); } } // execute - foreach (var createdBatch in batchOperations) + foreach (var createdBatch in tableTransactionsBatch) { if (createdBatch.Count() > 0) { - await table.ExecuteBatchAsync(createdBatch); + await tc.SubmitTransactionAsync(createdBatch); // notify delegate if (_delegate != null) @@ -397,10 +354,10 @@ private string GetTableName(string tableName) } } } - catch (StorageException ex) + catch (TableTransactionFailedException ex) { // check the exception - if (_autoCreateTable && ex.Message.StartsWith("0:The table specified does not exist", StringComparison.CurrentCulture)) + if (_autoCreateTable && ex.ErrorCode.Equals("TableNotFound")) { // try to create the table await CreateTableAsync(); @@ -419,19 +376,17 @@ private string GetTableName(string tableName) } } - public async Task DeleteAsync(T model) where T: new() - { - await DeleteAsync(new List() { model }); - } + public async Task DeleteAsync(T model) where T: new() + => await DeleteAsync(new List() { model }); public async Task DeleteAsync(IEnumerable models, bool allowMultiPartionRemoval = false) where T: new() { try { await this.StoreAsync(nStoreOperation.delete, models); - } catch(Exception e) + } catch(TableTransactionFailedException e) { - if (e.Message.Equals("All entities in a given batch must have the same partition key.") && allowMultiPartionRemoval) + if (e.ErrorCode.Equals("CommandsInBatchActOnDifferentPartitions") && allowMultiPartionRemoval) { // build a per partition key cache var partionKeyDictionary = new Dictionary>(); @@ -473,7 +428,7 @@ private string GetTableName(string tableName) _delegate.OnQuerying(typeof(T), partitionKey, rowKey, maxItems, continuationToken != null); // Retrieve a reference to the table. - CloudTable table = GetTableReference(GetTableName()); + var table = GetTableReference(GetTableName()); // lookup the entitymapper var entityMapper = _entityMapperRegistry[typeof(T)]; @@ -596,6 +551,11 @@ private CloudTable GetTableReference(string tableName) { return tableClient.GetTableReference(tableName); } + private TableClient GetTableClient(string tableName) + { + return new TableClient(_connectionString, tableName); + } + internal CloudTable RequestTableReference(string tableName) { var tableNamePatched = GetTableName(tableName);