diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS001StoreWithStaticEntityMapper.cs index 34dda33..ef7161b 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; 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/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.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/Attributes/RelatedTableAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs new file mode 100644 index 0000000..45610da --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/RelatedTableAttribute.cs @@ -0,0 +1,58 @@ +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; } + + /// + /// 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; } + + //TODO: + //public bool AutoDelete { 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; + } + + /// + /// + /// + /// 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; + } + + } +} \ No newline at end of file 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 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/Internal/StorageContextQueryNow.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs index e556dc0..541abdf 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -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; @@ -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..821614e 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -1,8 +1,10 @@ -using System; +using System; +using System.Collections; using System.Collections.Generic; 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; @@ -16,12 +18,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"); - else - return TableEntityDynamic.ToEntity(model, (context as StorageContext).GetEntityMapper()); + throw new Exception("Invalid interface implementation"); + else + 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(); @@ -41,35 +43,87 @@ 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 && 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 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(); } - public static T fromEntity(TableEntity entity, StorageEntityMapper entityMapper) where T : class, new() + private static async Task SaveRelatedTable(IStorageContext context, object o, PropertyInfo property) + { + if (o == null) + return; + + 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); + Type listType = typeof(List<>).MakeGenericType(new[] { endType }); + IList list = (IList)Activator.CreateInstance(listType); + list.Add(o); + o = list; + } + + 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; + } + + public static T fromEntity(TableEntity entity, StorageEntityMapper entityMapper, IStorageContext context) where T : class, new() { // create the target model var model = new T(); // 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, relatedTableAttribute)); else { if (!entity.ContainsKey(property.Name)) @@ -80,8 +134,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); } @@ -90,6 +148,68 @@ internal static class TableEntityDynamic return model; } + + private static object LoadRelatedTableProperty(IStorageContext context, T model, IEnumerable objectProperties, PropertyInfo property, RelatedTableAttribute relatedTableAttribute) where T : class, new() + { + 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 = 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 == relatedTableAttribute.PartitionKey).FirstOrDefault(); + if (partitionProperty != null) + extPartition = partitionProperty.GetValue(model).ToString(); + } + + 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) + 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("}}")) 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 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!