From 22618591a3a4002baa10c1a979ff579e70667ddc Mon Sep 17 00:00:00 2001 From: petero-dk <2478689+petero-dk@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:07:25 +0100 Subject: [PATCH 1/2] Add shared table feature --- .../ITS030SharedTable.cs | 57 +++++++++++++++++++ .../Models/MultipleModels.cs | 31 ++++++++++ .../Attributes/StorableAttribute.cs | 8 +++ .../Internal/StorageContextQueryNow.cs | 15 ++++- .../Serialization/TableEntityDynamic.cs | 26 ++++++--- .../StoargeEntityMapper.cs | 1 + .../StorageContextAttributeMapping.cs | 9 ++- README.md | 50 ++++++++++++++++ 8 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs create mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/MultipleModels.cs diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs new file mode 100644 index 0000000..e360e85 --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs @@ -0,0 +1,57 @@ +using System; +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 ITS030SharedTable + { + private readonly IStorageContext _rootContext; + + public ITS030SharedTable(IStorageContext context) + { + _rootContext = context; + + } + + + [Fact] + public async Task VerifyGetItem() + { + using (var scp = _rootContext.CreateChildContext()) + { + // set the tablename context + scp.SetTableContext(); + + // configure the entity mapper + scp.AddAttributeMapper(typeof(MultipleModelsBase)); + + + var model1 = new MultipleModels1() { P = "P1", Contact = "C1", Model1Field = "Model1Field" }; + var model2 = new MultipleModels2() { P = "P1", Contact = "C2", Model2Field = "Model2Field" }; + + + scp.EnableAutoCreateTable(); + + await scp.MergeOrInsertAsync(new[] {model1}); + await scp.MergeOrInsertAsync(new[] {model2}); + + + var result1 = await scp.QueryAsync("P1", "C1"); + Assert.Equivalent(model1, result1, true); + Assert.IsType(result1); + + var result2 = await scp.QueryAsync("P1", "C2"); + Assert.Equivalent(model2, result2, true); + Assert.IsType(result2); + + // cleanup + await scp.DropTableAsync(); + } + } + + } +} diff --git a/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/MultipleModels.cs b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/MultipleModels.cs new file mode 100644 index 0000000..07fd04e --- /dev/null +++ b/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/MultipleModels.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using CoreHelpers.WindowsAzure.Storage.Table.Attributes; + +namespace CoreHelpers.WindowsAzure.Storage.Table.Tests.Models +{ + + [Storable(TypeField = nameof(Type))] + public class MultipleModelsBase + { + [PartitionKey] + public string P { get; set; } = "Partition01"; + + [RowKey] + public string Contact { get; set; } = String.Empty; + + } + + public class MultipleModels1 : MultipleModelsBase + { + public string Model1Field { get; set; } = String.Empty; + + } + + public class MultipleModels2 : MultipleModelsBase + { + public string Model2Field { get; set; } = String.Empty; + + } + +} diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs index 41dd5f4..36ba837 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs @@ -6,9 +6,17 @@ namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes public class StorableAttribute : Attribute { public string Tablename { get; set; } + + public string TypeField { get; set; } = null; public StorableAttribute() {} + public StorableAttribute(string Tablename, string TypeField) + { + this.Tablename = Tablename; + this.TypeField = TypeField; + } + public StorableAttribute(string Tablename) { this.Tablename = Tablename; diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs index e556dc0..1eae2e4 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -94,8 +95,20 @@ private bool MoveNextInternal(bool initialPage) return MoveNextInternal(false); } + var entityMapper = _context.context.GetEntityMapper(); + // set the item - Current = TableEntityDynamic.fromEntity(_inPageEnumerator.Current, _context.context.GetEntityMapper()); + if (entityMapper.TypeField == null) + Current = TableEntityDynamic.fromEntity(_inPageEnumerator.Current, entityMapper); + else + { + var entity = _inPageEnumerator.Current; + var typeName = entity.GetString(entityMapper.TypeField); + Type type = Type.GetType(typeName); + MethodInfo method = typeof(TableEntityDynamic).GetMethod(nameof(TableEntityDynamic.fromEntity)); + MethodInfo genericMethod = method.MakeGenericMethod(type); + Current = genericMethod.Invoke(null, [_inPageEnumerator.Current, entityMapper] ) as T; + } // done return true; diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 503dce5..ace19b5 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(); @@ -29,12 +29,20 @@ internal static class TableEntityDynamic builder.AddPartitionKey(GetTableStorageDefaultProperty(entityMapper.PartitionKeyFormat, model)); builder.AddRowKey(GetTableStorageDefaultProperty(entityMapper.RowKeyFormat, model), entityMapper.RowKeyEncoding); + var modelType = model.GetType(); + // get all properties from model - IEnumerable objectProperties = model.GetType().GetTypeInfo().GetProperties(); + IEnumerable objectProperties = modelType.GetTypeInfo().GetProperties(); + + // it is not required and preferred NOT to have the type field in the model as we can ensure equality + builder.AddProperty(entityMapper.TypeField, modelType.AssemblyQualifiedName); // visit all properties foreach (PropertyInfo property in objectProperties) { + if (property.Name == entityMapper.TypeField) + continue; + if (ShouldSkipProperty(property)) continue; @@ -42,11 +50,11 @@ 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 - builder.AddProperty(property.Name, property.GetValue(model, null)); + builder.AddProperty(property.Name, property.GetValue(model, null)); } - + // build the result return builder.Build(); } @@ -58,13 +66,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,7 +88,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); diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs index 4003157..e3803de 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs @@ -9,6 +9,7 @@ public class StorageEntityMapper public String RowKeyFormat { get; set; } public nVirtualValueEncoding RowKeyEncoding { get; set; } public String TableName { get; set; } + public string TypeField { get; internal set; } public StorageEntityMapper() {} diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs index 995d5fb..8ef27ac 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs @@ -62,12 +62,18 @@ public void AddAttributeMapper(Type type) public void AddAttributeMapper(Type type, String optionalTablenameOverride) { + string typeField = null; + // get the concrete attribute var storableAttribute = type.GetTypeInfo().GetCustomAttribute(); if (String.IsNullOrEmpty(storableAttribute.Tablename)) { storableAttribute.Tablename = type.Name; } + if (!String.IsNullOrEmpty(storableAttribute.TypeField)) + { + typeField = storableAttribute.TypeField; + } // store the neded properties string partitionKeyFormat = null; @@ -111,7 +117,8 @@ public void AddAttributeMapper(Type type, String optionalTablenameOverride) TableName = String.IsNullOrEmpty(optionalTablenameOverride) ? storableAttribute.Tablename : optionalTablenameOverride, PartitionKeyFormat = partitionKeyFormat, RowKeyFormat = rowKeyFormat, - RowKeyEncoding = rowKeyEncoding + RowKeyEncoding = rowKeyEncoding, + TypeField = typeField, }); } diff --git a/README.md b/README.md index 99ebc39..ffb4969 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,56 @@ public class JObjectModel } ``` +## Store Multiple Objects Types in the same Table +If multiple objects share a common base class, it can be used to store them in the same table. The base class must be decorated with the Storable attribute with the `TypeField` parameter set. It is best practice to NOT include the type field in the model. + +```csharp + [Storable(TypeField = "Type")] + public class BaseModel + { + [PartitionKey] + public string P { get; set; } = "Partition01"; + + [RowKey] + public string R { get; set; } = String.Empty; + + } + + public class MultipleModels1 : MultipleModelsBase + { + public string Model1Field { get; set; } = String.Empty; + + } + + public class MultipleModels2 : MultipleModelsBase + { + public string Model2Field { get; set; } = String.Empty; + + } + +``` + +When saving and querying it is important to use the base class as the generic type. + +```csharp + using (var storageContext = new StorageContext(storageKey, storageSecret)) + { + storageContext.AddAttributeMapper(); + + storageContext.CreateTable(); + + storageContext.MergeOrInsert(new MultipleModels1() { R = "Row01", Model1Field = "Model1Field" }); + storageContext.MergeOrInsert(new MultipleModels2() { R = "Row02", Model2Field = "Model2Field" }); + + var result = storageContext.Query(); + + foreach (var r in result) + { + Console.WriteLine(r.GetType().Name); + } + } +``` + # Contributing to Azure Storage Table Fork as usual and go crazy! From 1a66694a466707601e9f9a6841f0b9c1d6fe5017 Mon Sep 17 00:00:00 2001 From: petero-dk <2478689+petero-dk@users.noreply.github.com> Date: Mon, 27 Nov 2023 20:46:08 +0100 Subject: [PATCH 2/2] fix bug --- .../Serialization/TableEntityDynamic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index ace19b5..af4118a 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -35,7 +35,8 @@ internal static class TableEntityDynamic IEnumerable objectProperties = modelType.GetTypeInfo().GetProperties(); // it is not required and preferred NOT to have the type field in the model as we can ensure equality - builder.AddProperty(entityMapper.TypeField, modelType.AssemblyQualifiedName); + if (!string.IsNullOrEmpty(entityMapper.TypeField)) + builder.AddProperty(entityMapper.TypeField, modelType.AssemblyQualifiedName); // visit all properties foreach (PropertyInfo property in objectProperties)