From 29d42b2422c99b9eb4d886bd635a6ae1901b464c Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 13 Oct 2022 20:56:28 +0200 Subject: [PATCH 01/12] add context to ToEntity --- .../Serialization/TableEntityDynamic.cs | 6 +++--- .../StorageContextDeleteEntities.cs | 2 +- .../StorageContextStoreEntities.cs | 10 +++++----- .../{StoargeEntityMapper.cs => StorageEntityMapper.cs} | 0 4 files changed, 9 insertions(+), 9 deletions(-) rename CoreHelpers.WindowsAzure.Storage.Table/{StoargeEntityMapper.cs => StorageEntityMapper.cs} (100%) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 503dce5..398180d 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -16,12 +16,12 @@ internal static class TableEntityDynamic public static TableEntity ToEntity(T model, IStorageContext context) where T : new() { if (context as StorageContext == null) - throw new Exception("Invalid interface implemnetation"); + throw new Exception("Invalid interface implementation"); else - return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper()); + return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper(), context); } - public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper) where T: new() + public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T: new() { var builder = new TableEntityBuilder(); diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextDeleteEntities.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextDeleteEntities.cs index 58a51cd..4e4c1cf 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextDeleteEntities.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextDeleteEntities.cs @@ -32,7 +32,7 @@ public partial class StorageContext : IStorageContext foreach (var model in models) { // convert the model to a dynamic entity - var t = TableEntityDynamic.ToEntity(model, entityMapper); + var t = TableEntityDynamic.ToEntity(model, entityMapper, this); // lookup the partitionkey list if (!partionKeyDictionary.ContainsKey(t.PartitionKey)) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextStoreEntities.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextStoreEntities.cs index 1979d7a..2620241 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextStoreEntities.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextStoreEntities.cs @@ -57,19 +57,19 @@ public partial class StorageContext : IStorageContext switch (storaeOperationType) { case nStoreOperation.insertOperation: - tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Add, TableEntityDynamic.ToEntity(model, entityMapper))); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Add, TableEntityDynamic.ToEntity(model, entityMapper, this))); break; case nStoreOperation.insertOrReplaceOperation: - tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertReplace, TableEntityDynamic.ToEntity(model, entityMapper))); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertReplace, TableEntityDynamic.ToEntity(model, entityMapper, this))); break; case nStoreOperation.mergeOperation: - tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpdateMerge, TableEntityDynamic.ToEntity(model, entityMapper))); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpdateMerge, TableEntityDynamic.ToEntity(model, entityMapper, this))); break; case nStoreOperation.mergeOrInserOperation: - tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertMerge, TableEntityDynamic.ToEntity(model, entityMapper))); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.UpsertMerge, TableEntityDynamic.ToEntity(model, entityMapper, this))); break; case nStoreOperation.delete: - tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Delete, TableEntityDynamic.ToEntity(model, entityMapper))); + tableTransactions.Add(new TableTransactionAction(TableTransactionActionType.Delete, TableEntityDynamic.ToEntity(model, entityMapper, this))); break; } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageEntityMapper.cs similarity index 100% rename from CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs rename to CoreHelpers.WindowsAzure.Storage.Table/StorageEntityMapper.cs From 75e9cab4686ece9380af089b192718addae42b2e Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 13 Oct 2022 21:07:00 +0200 Subject: [PATCH 02/12] context to fromentity --- .../Internal/StorageContextQueryNow.cs | 2 +- .../Serialization/TableEntityDynamic.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs index e556dc0..d2198f9 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -95,7 +95,7 @@ private bool MoveNextInternal(bool initialPage) } // set the item - Current = TableEntityDynamic.fromEntity(_inPageEnumerator.Current, _context.context.GetEntityMapper()); + Current = TableEntityDynamic.fromEntity(_inPageEnumerator.Current, _context.context.GetEntityMapper(), _context.context); // done return true; diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 398180d..35ce565 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -51,7 +51,7 @@ internal static class TableEntityDynamic return builder.Build(); } - public static T fromEntity(TableEntity entity, StorageEntityMapper entityMapper) where T : class, new() + public static T fromEntity(TableEntity entity, StorageEntityMapper entityMapper, IStorageContext context) where T : class, new() { // create the target model var model = new T(); From 3549b5efc8ba24afeddcd6995e5aab33accf1762 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Sun, 16 Oct 2022 12:50:51 +0200 Subject: [PATCH 03/12] Load related item(s) --- .../Attributes/RelatedTableAttribute.cs | 37 ++++++++ .../Extensions/TypeExtensions.cs | 22 ++++- .../Internal/DynamicLazy.cs | 15 ++++ .../Serialization/TableEntityDynamic.cs | 89 +++++++++++++++++-- 4 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table/Internal/DynamicLazy.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..3212410 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs @@ -0,0 +1,37 @@ +using System; +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; + } + + } +} \ No newline at end of file diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TypeExtensions.cs b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TypeExtensions.cs index e0d9693..8e782e1 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TypeExtensions.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Extensions/TypeExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; + namespace CoreHelpers.WindowsAzure.Storage.Table.Extensions { public enum ExportEdmType @@ -34,7 +36,25 @@ public static ExportEdmType GetEdmPropertyType(this Type type) else if (type == typeof(Int64)) return ExportEdmType.Int64; else - throw new NotImplementedException($"Datatype {type.ToString()} not supporter"); + throw new NotImplementedException($"Datatype {type.ToString()} not supporter"); + } + + public static bool IsDerivedFromGenericParent(this Type type, Type parentType) + { + if (!parentType.IsGenericType) + { + throw new ArgumentException("type must be generic", "parentType"); + } + if (type == null || type == typeof(object)) + { + return false; + } + if (type.IsGenericType && type.GetGenericTypeDefinition() == parentType) + { + return true; + } + return type.BaseType.IsDerivedFromGenericParent(parentType) + || type.GetInterfaces().Any(t => t.IsDerivedFromGenericParent(parentType)); } } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Internal/DynamicLazy.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/DynamicLazy.cs new file mode 100644 index 0000000..168e245 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/DynamicLazy.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Internal +{ + internal class DynamicLazy : Lazy + { + public DynamicLazy(Func factory) : base(() => (T)factory()) + { + + } + + } +} \ No newline at end of file diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 35ce565..1149dd7 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -8,6 +8,7 @@ using CoreHelpers.WindowsAzure.Storage.Table.Extensions; using CoreHelpers.WindowsAzure.Storage.Table.Internal; using HandlebarsDotNet; +using Newtonsoft.Json.Linq; namespace CoreHelpers.WindowsAzure.Storage.Table.Serialization { @@ -17,11 +18,11 @@ internal static class TableEntityDynamic { if (context as StorageContext == null) throw new Exception("Invalid interface implementation"); - else + else return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper(), context); } - public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T: new() + public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T : new() { var builder = new TableEntityBuilder(); @@ -41,12 +42,17 @@ internal static class TableEntityDynamic // 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(); + + var relatedTableAttribute = property.GetCustomAttributes().Where(a => a is RelatedTableAttribute).Select(a => a as RelatedTableAttribute).FirstOrDefault(); + if (virtualTypeAttribute != null) - virtualTypeAttribute.WriteProperty(property, model, builder); + virtualTypeAttribute.WriteProperty(property, model, builder); + else if (relatedTableAttribute != null) + continue; else - builder.AddProperty(property.Name, property.GetValue(model, null)); + builder.AddProperty(property.Name, property.GetValue(model, null)); } - + // build the result return builder.Build(); } @@ -58,18 +64,23 @@ internal static class TableEntityDynamic // 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(); + + var relatedTableAttribute = property.GetCustomAttributes().Where(a => a is RelatedTableAttribute).Select(a => a as RelatedTableAttribute).FirstOrDefault(); + if (virtualTypeAttribute != null) virtualTypeAttribute.ReadProperty(entity, property, model); + else if (relatedTableAttribute != null) + property.SetValue(model, LoadRelatedTableProperty(context, model, objectProperties, property)); else { if (!entity.ContainsKey(property.Name)) @@ -80,7 +91,7 @@ internal static class TableEntityDynamic if (!entity.TryGetValue(property.Name, out objectValue)) continue; - if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?) ) + if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?)) property.SetDateTimeOffsetValue(model, objectValue); else property.SetValue(model, objectValue); @@ -90,6 +101,68 @@ internal static class TableEntityDynamic return model; } + private static object LoadRelatedTableProperty(IStorageContext context, T model, IEnumerable objectProperties, PropertyInfo property) where T : class, new() + { + var relatedTable = property.GetCustomAttribute(); + var isLazy = false; + var isEnumerable = false; + + + Type endType; + if (property.PropertyType.IsDerivedFromGenericParent(typeof(Lazy<>))) + { + endType = property.PropertyType.GetTypeInfo().GenericTypeArguments[0]; + isLazy = true; + } + else + endType = property.PropertyType; + + if (endType.IsDerivedFromGenericParent(typeof(IEnumerable<>))) + isEnumerable = true; + + // 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).ToString(); + } + + string extRowKey = relatedTable.RowKey ?? endType.Name; + // if the row key is the name of a property on the model, get the value + var rowkeyProperty = objectProperties.Where((pi) => pi.Name == extRowKey).FirstOrDefault(); + if (rowkeyProperty != null) + extRowKey = rowkeyProperty.GetValue(model).ToString(); + + var method = typeof(StorageContext).GetMethod(nameof(StorageContext.QueryAsync), + isEnumerable ? + new[] { typeof(string), typeof(int) } : + new[] { typeof(string), typeof(string), typeof(int) }); + var generic = method.MakeGenericMethod(endType); + + // if the property is a lazy type, create the lazy initialization + if (isLazy) + { + var lazyType = typeof(DynamicLazy<>); + var constructed = lazyType.MakeGenericType(endType); + + object o = Activator.CreateInstance(constructed, new Func(() => + { + var waitable = (dynamic)generic.Invoke(context, new object[] { extPartition, extRowKey, 1 }); + return waitable.Result; + })); + return o; + + } + else + { + var waitable = (dynamic)generic.Invoke(context, new object[] { extPartition, extRowKey, 1 }); + return waitable.Result; + } + } + private static S GetTableStorageDefaultProperty(string format, T model) where S : class { if (typeof(S) == typeof(string) && format.Contains("{{") && format.Contains("}}")) From 917886d8f130a3af5b9a839c5b941f05177b4d54 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Sun, 16 Oct 2022 13:00:51 +0200 Subject: [PATCH 04/12] Add entry to readme --- .../Serialization/TableEntityDynamic.cs | 1 - README.md | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 1149dd7..8b83b6a 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -8,7 +8,6 @@ using CoreHelpers.WindowsAzure.Storage.Table.Extensions; using CoreHelpers.WindowsAzure.Storage.Table.Internal; using HandlebarsDotNet; -using Newtonsoft.Json.Linq; namespace CoreHelpers.WindowsAzure.Storage.Table.Serialization { diff --git a/README.md b/README.md index 99ebc39..eab81e6 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,55 @@ public class JObjectModel } ``` +## 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; } +} +``` + # Contributing to Azure Storage Table Fork as usual and go crazy! From 3f4f969ed2efd4a6297926ba07716d37a35ad2cc Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Sun, 16 Oct 2022 13:26:15 +0200 Subject: [PATCH 05/12] test fails test --- .../ITS001StoreWithStaticEntityMapper.cs | 5 +++++ .../Serialization/TableEntityDynamic.cs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs index 34dda33..fb8e556 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using CoreHelpers.WindowsAzure.Storage.Table.Tests.Contracts; using CoreHelpers.WindowsAzure.Storage.Table.Tests.Delegates; using CoreHelpers.WindowsAzure.Storage.Table.Tests.Extensions; @@ -63,6 +64,10 @@ public async Task VerifyStaticEntityMapperOperations() // ensure we are empty var resultEmpty = await scp.QueryAsync(); + foreach (var i in resultEmpty) + { + Debug.WriteLine(i.FirstName); + } Assert.Empty(resultEmpty); // inser the model diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 8b83b6a..0178458 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -46,8 +46,8 @@ internal static class TableEntityDynamic if (virtualTypeAttribute != null) virtualTypeAttribute.WriteProperty(property, model, builder); - else if (relatedTableAttribute != null) - continue; + //else if (relatedTableAttribute != null) + //continue; else builder.AddProperty(property.Name, property.GetValue(model, null)); } @@ -78,8 +78,8 @@ internal static class TableEntityDynamic if (virtualTypeAttribute != null) virtualTypeAttribute.ReadProperty(entity, property, model); - else if (relatedTableAttribute != null) - property.SetValue(model, LoadRelatedTableProperty(context, model, objectProperties, property)); + //else if (relatedTableAttribute != null) + // property.SetValue(model, LoadRelatedTableProperty(context, model, objectProperties, property)); else { if (!entity.ContainsKey(property.Name)) From f73b3375c0f799629e36eaad8e3ef8db1f2033e7 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Mon, 17 Oct 2022 14:54:56 +0200 Subject: [PATCH 06/12] fix azurite filter bug (must be null) --- .../Internal/StorageContextQueryNow.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs index d2198f9..53004fd 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -140,9 +140,12 @@ private void InitializePageEnumeratorIfNeeded() // evaluate the maxItems int? maxPerPage = _context.maxPerPage.HasValue && _context.maxPerPage.Value > 0 ? _context.maxPerPage : null; - + + // fix Azurite bug + var filter = string.IsNullOrWhiteSpace(_context.filter) ? null : _context.filter; // start the query - _pageEnumerator = tc.Query(_context.filter, maxPerPage, _context.select, _context.cancellationToken).AsPages().GetEnumerator(); + _pageEnumerator = tc.Query(filter, maxPerPage, _context.select, _context.cancellationToken).AsPages().GetEnumerator(); + } } From 2acdc6c131485164d0ff4c2744a3ae9c21f32f22 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Mon, 17 Oct 2022 20:15:38 +0200 Subject: [PATCH 07/12] Auto save entity, defaults to false --- .../ITS001StoreWithStaticEntityMapper.cs | 4 -- .../Attributes/RelatedTableAttribute.cs | 20 ++++++++ .../Serialization/TableEntityDynamic.cs | 49 +++++++++++++++---- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs index fb8e556..ef7161b 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs @@ -64,10 +64,6 @@ public async Task VerifyStaticEntityMapperOperations() // ensure we are empty var resultEmpty = await scp.QueryAsync(); - foreach (var i in resultEmpty) - { - Debug.WriteLine(i.FirstName); - } Assert.Empty(resultEmpty); // inser the model diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs index 3212410..82b3059 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs @@ -13,6 +13,11 @@ public class RelatedTableAttribute : Attribute /// public string RowKey { get; set; } + /// + /// On saving the parent entity also save the related tables, currently not supported with null values in , Defaults to False. + /// + public bool AutoSave { get; set; } + /// /// /// @@ -20,6 +25,7 @@ public class RelatedTableAttribute : Attribute public RelatedTableAttribute(string partitionKey) { PartitionKey = partitionKey; + AutoSave = false; } /// @@ -31,6 +37,20 @@ public RelatedTableAttribute(string partitionKey, string rowKey) { PartitionKey = partitionKey; RowKey = rowKey; + AutoSave = false; + } + + /// + /// + /// + /// 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. + /// Sets the autosave property + public RelatedTableAttribute(string partitionKey, string rowKey, bool autoSave) + { + PartitionKey = partitionKey; + RowKey = rowKey; + AutoSave = autoSave; } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 0178458..0051345 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using System.Threading.Tasks; using Azure.Data.Tables; using CoreHelpers.WindowsAzure.Storage.Table.Attributes; using CoreHelpers.WindowsAzure.Storage.Table.Extensions; @@ -21,7 +22,7 @@ internal static class TableEntityDynamic return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper(), context); } - public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T : new() + public static async TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T : new() { var builder = new TableEntityBuilder(); @@ -46,8 +47,9 @@ internal static class TableEntityDynamic if (virtualTypeAttribute != null) virtualTypeAttribute.WriteProperty(property, model, builder); - //else if (relatedTableAttribute != null) - //continue; + else if (relatedTableAttribute != null && relatedTableAttribute.) + // TODO: Implicit save rowkey and partitionkey (will need to get from saved model) + await SaveRelatedTable(context, property.GetValue(model, null), property); else builder.AddProperty(property.Name, property.GetValue(model, null)); } @@ -56,6 +58,33 @@ internal static class TableEntityDynamic return builder.Build(); } + private static async Task SaveRelatedTable(IStorageContext context, object o, PropertyInfo property) + { + Type endType; + if (property.PropertyType.IsDerivedFromGenericParent(typeof(Lazy<>))) + { + endType = property.PropertyType.GetTypeInfo().GenericTypeArguments[0]; + var lazy = (Lazy)o; + if (!lazy.IsValueCreated) + return; //if the value is not created we should not load it just to store it. + o = lazy.Value; + } + else + endType = property.PropertyType; + + var enumerableType = endType; + if (endType.IsDerivedFromGenericParent(typeof(IEnumerable<>))) + endType = endType.GetTypeInfo().GenericTypeArguments[0]; + else + enumerableType = typeof(IEnumerable<>).MakeGenericType(endType); + + var method = typeof(StorageContext).GetMethod(nameof(StorageContext.StoreAsync), + new[] { typeof(nStoreOperation), enumerableType }); + var generic = method.MakeGenericMethod(endType); + var waitable = (Task)generic.Invoke(context, new object[] { nStoreOperation.insertOrReplaceOperation, o }); + await waitable; + } + public static T fromEntity(TableEntity entity, StorageEntityMapper entityMapper, IStorageContext context) where T : class, new() { // create the target model @@ -78,8 +107,8 @@ internal static class TableEntityDynamic if (virtualTypeAttribute != null) virtualTypeAttribute.ReadProperty(entity, property, model); - //else if (relatedTableAttribute != null) - // property.SetValue(model, LoadRelatedTableProperty(context, model, objectProperties, property)); + else if (relatedTableAttribute != null) + property.SetValue(model, LoadRelatedTableProperty(context, model, objectProperties, property, relatedTableAttribute)); else { if (!entity.ContainsKey(property.Name)) @@ -100,9 +129,9 @@ internal static class TableEntityDynamic return model; } - private static object LoadRelatedTableProperty(IStorageContext context, T model, IEnumerable objectProperties, PropertyInfo property) where T : class, new() + + private static object LoadRelatedTableProperty(IStorageContext context, T model, IEnumerable objectProperties, PropertyInfo property, RelatedTableAttribute relatedTableAttribute) where T : class, new() { - var relatedTable = property.GetCustomAttribute(); var isLazy = false; var isEnumerable = false; @@ -120,16 +149,16 @@ internal static class TableEntityDynamic isEnumerable = true; // determine the partition key - string extPartition = relatedTable.PartitionKey; + string extPartition = relatedTableAttribute.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(); + var partitionProperty = objectProperties.Where((pi) => pi.Name == relatedTableAttribute.PartitionKey).FirstOrDefault(); if (partitionProperty != null) extPartition = partitionProperty.GetValue(model).ToString(); } - string extRowKey = relatedTable.RowKey ?? endType.Name; + string extRowKey = relatedTableAttribute.RowKey ?? endType.Name; // if the row key is the name of a property on the model, get the value var rowkeyProperty = objectProperties.Where((pi) => pi.Name == extRowKey).FirstOrDefault(); if (rowkeyProperty != null) From dd18af6e34fe2653f0fa87686bedcf3c2d6bf909 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Mon, 17 Oct 2022 20:23:32 +0200 Subject: [PATCH 08/12] skip if not autosave on store --- .../Serialization/TableEntityDynamic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 0051345..719dafe 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -47,7 +47,7 @@ internal static class TableEntityDynamic if (virtualTypeAttribute != null) virtualTypeAttribute.WriteProperty(property, model, builder); - else if (relatedTableAttribute != null && relatedTableAttribute.) + else if (relatedTableAttribute != null && relatedTableAttribute.AutoSave) // TODO: Implicit save rowkey and partitionkey (will need to get from saved model) await SaveRelatedTable(context, property.GetValue(model, null), property); else From 0b6fed16f2f62fa2dc5f938e1f49776d5ed58c37 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Mon, 17 Oct 2022 21:53:02 +0200 Subject: [PATCH 09/12] bugfix --- .../Serialization/TableEntityDynamic.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 719dafe..9f0a4eb 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -22,7 +22,7 @@ internal static class TableEntityDynamic return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper(), context); } - public static async TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T : new() + public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper, IStorageContext context) where T : new() { var builder = new TableEntityBuilder(); @@ -49,7 +49,7 @@ internal static class TableEntityDynamic virtualTypeAttribute.WriteProperty(property, model, builder); else if (relatedTableAttribute != null && relatedTableAttribute.AutoSave) // TODO: Implicit save rowkey and partitionkey (will need to get from saved model) - await SaveRelatedTable(context, property.GetValue(model, null), property); + SaveRelatedTable(context, property.GetValue(model, null), property).Wait(); else builder.AddProperty(property.Name, property.GetValue(model, null)); } From 9e23d4bdd94718fe593b5af2db0750266e94f1c5 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 20 Oct 2022 14:54:37 +0200 Subject: [PATCH 10/12] Implement enum storage --- .../ITS025StoreEnum.cs | 59 +++++++++++++++++++ .../Models/UserModel4.cs | 28 +++++++++ .../Internal/StorageContextQueryNow.cs | 6 +- .../Serialization/TableEntityDynamic.cs | 22 ++++--- 4 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS025StoreEnum.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/UserModel4.cs diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS025StoreEnum.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS025StoreEnum.cs new file mode 100644 index 0000000..f11b792 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS025StoreEnum.cs @@ -0,0 +1,59 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Contracts; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Extensions; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Models; +using Xunit.DependencyInjection; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests +{ + [Startup(typeof(Startup))] + [Collection("Sequential")] + public class ITS025StoreEnum + { + private readonly IStorageContext _rootContext; + + public ITS025StoreEnum(IStorageContext context) + { + _rootContext = context; + } + + [Fact] + public async Task VerifyAttributeMapper() + { + using (var storageContext = _rootContext.CreateChildContext()) + { + // set the tablename context + storageContext.SetTableContext(); + + // create a new user + var user = new UserModel4() { FirstName = "Egon", LastName = "Mueller", Contact = "em@acme.org", UserType = UserTypeEnum.Pro }; + + // ensure we are using the attributes + storageContext.AddAttributeMapper(); + + // ensure the table exists + await storageContext.CreateTableAsync(); + + // inser the model + await storageContext.MergeOrInsertAsync(user); + + // query all + var result = await storageContext.QueryAsync(); + Assert.Single(result); + Assert.Equal("Egon", result.First().FirstName); + Assert.Equal("Mueller", result.First().LastName); + Assert.Equal("em@acme.org", result.First().Contact); + Assert.Equal(UserTypeEnum.Pro, result.First().UserType); + + // Clean up + await storageContext.DeleteAsync(result); + result = await storageContext.QueryAsync(); + Assert.NotNull(result); + Assert.Empty(result); + + await storageContext.DropTableAsync(); + } + } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/UserModel4.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/UserModel4.cs new file mode 100644 index 0000000..05f4cc5 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/UserModel4.cs @@ -0,0 +1,28 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests.Models +{ + + public enum UserTypeEnum + { + Free, + Pro + } + + [Storable()] + public class UserModel4 + { + [PartitionKey] + public string P { get; set; } = "Partition01"; + + [RowKey] + public string Contact { get; set; } = String.Empty; + + public string FirstName { get; set; } = String.Empty; + public string LastName { get; set; } = String.Empty; + + public UserTypeEnum UserType { get; set; } + + } +} diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs index e556dc0..127e257 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -140,9 +140,11 @@ private void InitializePageEnumeratorIfNeeded() // evaluate the maxItems int? maxPerPage = _context.maxPerPage.HasValue && _context.maxPerPage.Value > 0 ? _context.maxPerPage : null; - + + // fix Azurite bug + var filter = string.IsNullOrWhiteSpace(_context.filter) ? null : _context.filter; // start the query - _pageEnumerator = tc.Query(_context.filter, maxPerPage, _context.select, _context.cancellationToken).AsPages().GetEnumerator(); + _pageEnumerator = tc.Query(filter, maxPerPage, _context.select, _context.cancellationToken).AsPages().GetEnumerator(); } } diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 503dce5..68b5515 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -17,11 +17,11 @@ internal static class TableEntityDynamic { if (context as StorageContext == null) throw new Exception("Invalid interface implemnetation"); - else + else return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper()); } - public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper) where T: new() + public static TableEntity ToEntity(T model, StorageEntityMapper entityMapper) where T : new() { var builder = new TableEntityBuilder(); @@ -42,11 +42,13 @@ internal static class TableEntityDynamic // 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); + virtualTypeAttribute.WriteProperty(property, model, builder); + else if (property.PropertyType.IsEnum) + builder.AddProperty(property.Name, property.GetValue(model, null).ToString()); else - builder.AddProperty(property.Name, property.GetValue(model, null)); + builder.AddProperty(property.Name, property.GetValue(model, null)); } - + // build the result return builder.Build(); } @@ -58,13 +60,13 @@ internal static class TableEntityDynamic // 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(); @@ -80,8 +82,12 @@ internal static class TableEntityDynamic if (!entity.TryGetValue(property.Name, out objectValue)) continue; - if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?) ) + if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?)) property.SetDateTimeOffsetValue(model, objectValue); + else if (property.PropertyType.IsEnum && int.TryParse(objectValue.ToString(), out var intEnum) && property.PropertyType.IsEnumDefined(intEnum)) + property.SetValue(model, Enum.ToObject(property.PropertyType, intEnum)); + else if (property.PropertyType.IsEnum && property.PropertyType.IsEnumDefined(objectValue.ToString())) + property.SetValue(model, Enum.Parse(property.PropertyType, objectValue.ToString())); else property.SetValue(model, objectValue); } From 0aeec8d7c602dffd3149e2d839bcc40b53f49319 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 20 Oct 2022 14:57:44 +0200 Subject: [PATCH 11/12] set version --- .../CoreHelpers.WindowsAzure.Storage.Table.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj index 3dac931..3225b80 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj +++ b/CoreHelpers.WindowsAzure.Storage.Table/CoreHelpers.WindowsAzure.Storage.Table.csproj @@ -11,6 +11,8 @@ This projects implements an abstraction for Azure Storage Tables to use POCOs because deriving every entity from ITableEntity or TableEntity looks like a step backwards. The current implementation is intended to be an abstraction to store every existing entity into Azure Table Store. poco dotnet-core dotnet azure azure-storage azure-table-storage (c) Dirk Eisenberg + 6.0.4-relatedtable + false From 0b9f186f774bf55467fe1547117a6bd332aff0e9 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 20 Oct 2022 15:49:30 +0200 Subject: [PATCH 12/12] Added tests, fixed store bug(s) --- .../ITS026RelatedTable.cs | 121 ++++++++++++++++++ .../Models/DemoModel3.cs | 22 ++++ .../Models/DemoModel4.cs | 22 ++++ .../Attributes/RelatedTableAttribute.cs | 5 +- .../Serialization/TableEntityDynamic.cs | 17 ++- 5 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS026RelatedTable.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel3.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel4.cs diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS026RelatedTable.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS026RelatedTable.cs new file mode 100644 index 0000000..38a1565 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS026RelatedTable.cs @@ -0,0 +1,121 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Contracts; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Extensions; +using CoreHelpers.WindowsAzure.Storage.Table.Tests.Models; +using Xunit.DependencyInjection; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests +{ + [Startup(typeof(Startup))] + [Collection("Sequential")] + public class ITS026RelatedTable + { + private readonly IStorageContext _rootContext; + + public ITS026RelatedTable(IStorageContext context) + { + _rootContext = context; + } + + [Fact] + public async Task ReadRelatedTable() + { + using (var storageContext = _rootContext.CreateChildContext()) + { + // set the tablename context + storageContext.SetTableContext(); + // + // create a new user + var user = new UserModel2() { FirstName = "Egon", LastName = "Mueller", Contact = "em@acme.org" }; + var demo = new DemoModel3() { P = "P2", R = "R2", UserContact = "em@acme.org" }; + + // ensure we are using the attributes + storageContext.AddAttributeMapper(); + + // ensure the tables exists + await storageContext.CreateTableAsync(); + await storageContext.CreateTableAsync(); + + // inser the models + await storageContext.MergeOrInsertAsync(user); + await storageContext.MergeOrInsertAsync(demo); + + // query all + var result = await storageContext.QueryAsync(); + Assert.Single(result); + Assert.Equal("Egon", result.First().User?.FirstName); + Assert.Equal("Mueller", result.First().User?.LastName); + Assert.Equal("em@acme.org", result.First().User?.Contact); + + // Clean up + user = result.First().User; + if (user != null) + await storageContext.DeleteAsync(user); + + var userResult = await storageContext.QueryAsync(); + Assert.NotNull(userResult); + Assert.Empty(userResult); + + + await storageContext.DeleteAsync(result); + result = await storageContext.QueryAsync(); + Assert.NotNull(result); + Assert.Empty(result); + + await storageContext.DropTableAsync(); + await storageContext.DropTableAsync(); + } + } + + + [Fact] + public async Task WriteRelatedTable() + { + using (var storageContext = _rootContext.CreateChildContext()) + { + // set the tablename context + storageContext.SetTableContext(); + // + // create a new user + var user = new UserModel2() { FirstName = "Egon", LastName = "Mueller", Contact = "em@acme.org" }; + var demo = new DemoModel4() { P = "P2", R = "R2", UserContact = "em@acme.org", User = user }; + + // ensure we are using the attributes + storageContext.AddAttributeMapper(); + + // ensure the tables exists + await storageContext.CreateTableAsync(); + await storageContext.CreateTableAsync(); + + // inser the model + await storageContext.MergeOrInsertAsync(demo); + + // query all + var result = await storageContext.QueryAsync(); + Assert.Single(result); + Assert.Equal("Egon", result.First().User?.FirstName); + Assert.Equal("Mueller", result.First().User?.LastName); + Assert.Equal("em@acme.org", result.First().User?.Contact); + + // Clean up + user = result.First().User; + if (user != null) + await storageContext.DeleteAsync(user); + + var userResult = await storageContext.QueryAsync(); + Assert.NotNull(userResult); + Assert.Empty(userResult); + + + await storageContext.DeleteAsync(result); + result = await storageContext.QueryAsync(); + Assert.NotNull(result); + Assert.Empty(result); + + await storageContext.DropTableAsync(); + await storageContext.DropTableAsync(); + } + } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel3.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel3.cs new file mode 100644 index 0000000..6dbb16f --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel3.cs @@ -0,0 +1,22 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests.Models +{ + [Storable] + public class DemoModel3 + { + + [PartitionKey] + public string P { get; set; } = "P1"; + + [RowKey] + public string R { get; set; } = "R1"; + + public string UserContact { get; set; } = "em@acme.org"; + + [RelatedTable("Partition01", RowKey = "UserContact")] + public UserModel2? User { get; set; } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel4.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel4.cs new file mode 100644 index 0000000..55102ab --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/DemoModel4.cs @@ -0,0 +1,22 @@ +using System; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests.Models +{ + [Storable] + public class DemoModel4 + { + + [PartitionKey] + public string P { get; set; } = "P1"; + + [RowKey] + public string R { get; set; } = "R1"; + + public string UserContact { get; set; } = "em@acme.org"; + + [RelatedTable("Partition01", RowKey = "UserContact", AutoSave = true)] + public UserModel2? User { get; set; } + } +} + diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs index 82b3059..45610da 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs @@ -18,6 +18,9 @@ public class RelatedTableAttribute : Attribute /// public bool AutoSave { get; set; } + //TODO: + //public bool AutoDelete { get; set; } + /// /// /// @@ -25,7 +28,6 @@ public class RelatedTableAttribute : Attribute public RelatedTableAttribute(string partitionKey) { PartitionKey = partitionKey; - AutoSave = false; } /// @@ -37,7 +39,6 @@ public RelatedTableAttribute(string partitionKey, string rowKey) { PartitionKey = partitionKey; RowKey = rowKey; - AutoSave = false; } /// diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 9f0a4eb..f28dd85 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -50,6 +51,8 @@ internal static class TableEntityDynamic else if (relatedTableAttribute != null && relatedTableAttribute.AutoSave) // TODO: Implicit save rowkey and partitionkey (will need to get from saved model) SaveRelatedTable(context, property.GetValue(model, null), property).Wait(); + else if (relatedTableAttribute != null) + continue; else builder.AddProperty(property.Name, property.GetValue(model, null)); } @@ -60,6 +63,9 @@ internal static class TableEntityDynamic private static async Task SaveRelatedTable(IStorageContext context, object o, PropertyInfo property) { + if (o == null) + return; + Type endType; if (property.PropertyType.IsDerivedFromGenericParent(typeof(Lazy<>))) { @@ -76,10 +82,17 @@ private static async Task SaveRelatedTable(IStorageContext context, object o, Pr if (endType.IsDerivedFromGenericParent(typeof(IEnumerable<>))) endType = endType.GetTypeInfo().GenericTypeArguments[0]; else + { enumerableType = typeof(IEnumerable<>).MakeGenericType(endType); + Type listType = typeof(List<>).MakeGenericType(new[] { endType }); + IList list = (IList)Activator.CreateInstance(listType); + list.Add(o); + o = list; + } - var method = typeof(StorageContext).GetMethod(nameof(StorageContext.StoreAsync), - new[] { typeof(nStoreOperation), enumerableType }); + var method = typeof(StorageContext) + .GetMethods() + .Single(m => m.Name == nameof(StorageContext.StoreAsync) && m.IsGenericMethodDefinition); var generic = method.MakeGenericMethod(endType); var waitable = (Task)generic.Invoke(context, new object[] { nStoreOperation.insertOrReplaceOperation, o }); await waitable;