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<MultipleModelsBase>(new[] {model1});
+                await scp.MergeOrInsertAsync<MultipleModelsBase>(new[] {model2});
+
+
+                var result1 = await scp.QueryAsync<MultipleModelsBase>("P1", "C1");
+                Assert.Equivalent(model1, result1, true);
+                Assert.IsType<MultipleModels1>(result1);
+
+                var result2 = await scp.QueryAsync<MultipleModelsBase>("P1", "C2");
+                Assert.Equivalent(model2, result2, true);
+                Assert.IsType<MultipleModels2>(result2);
+
+                // cleanup
+                await scp.DropTableAsync<MultipleModelsBase>();
+            }
+        }
+
+    }
+}
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<T>();
+
                 // set the item
-                Current = TableEntityDynamic.fromEntity<T>(_inPageEnumerator.Current, _context.context.GetEntityMapper<T>());
+                if (entityMapper.TypeField == null)
+                    Current = TableEntityDynamic.fromEntity<T>(_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<T>(model, (context as StorageContext).GetEntityMapper<T>());
         }
 
-        public static TableEntity ToEntity<T>(T model, StorageEntityMapper entityMapper) where T: new()
+        public static TableEntity ToEntity<T>(T model, StorageEntityMapper entityMapper) where T : new()
         {
             var builder = new TableEntityBuilder();
 
@@ -29,12 +29,20 @@ internal static class TableEntityDynamic
             builder.AddPartitionKey(GetTableStorageDefaultProperty<string, T>(entityMapper.PartitionKeyFormat, model));
             builder.AddRowKey(GetTableStorageDefaultProperty<string, T>(entityMapper.RowKeyFormat, model), entityMapper.RowKeyEncoding);
 
+            var modelType = model.GetType();
+
             // get all properties from model 
-            IEnumerable<PropertyInfo> objectProperties = model.GetType().GetTypeInfo().GetProperties();
+            IEnumerable<PropertyInfo> 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<IVirtualTypeAttribute>();
                 if (virtualTypeAttribute != null)
-                    virtualTypeAttribute.WriteProperty<T>(property, model, builder);                                                   
+                    virtualTypeAttribute.WriteProperty<T>(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<PropertyInfo> 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<IVirtualTypeAttribute>();
@@ -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<StorableAttribute>();
             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<BaseModel>();
+
+		storageContext.MergeOrInsert<BaseModel>(new MultipleModels1() { R = "Row01", Model1Field = "Model1Field" });
+		storageContext.MergeOrInsert<BaseModel>(new MultipleModels2() { R = "Row02", Model2Field = "Model2Field" });
+
+		var result = storageContext.Query<BaseModel>();
+
+		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<PropertyInfo> 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)