From 9e23d4bdd94718fe593b5af2db0750266e94f1c5 Mon Sep 17 00:00:00 2001 From: Peter Obel Date: Thu, 20 Oct 2022 14:54:37 +0200 Subject: [PATCH 1/4] 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 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 2/4] 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 f400c606822f9c55f0a000690040f68642e1385e Mon Sep 17 00:00:00 2001 From: petero-dk <2478689+petero-dk@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:37:34 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ffb4969..2e43bf9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ [![Build Status](https://github.com/CoreHelpers/AzureStorageTable/actions/workflows/ci-build.yml/badge.svg)](https://github.com/CoreHelpers/AzureStorageTable/actions/workflows/ci-build.yml) +! NOTICE THIS IS A CUSTOM BUILD OF AzureStorageTable that contains the following additional features: +* Multiple Types in the same table (Base objects) + # AzureStorageTable 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 From e2f40d90946a6fa14af83c37c79816a2da70a270 Mon Sep 17 00:00:00 2001 From: petero-dk <2478689+petero-dk@users.noreply.github.com> Date: Mon, 27 Nov 2023 19:55:04 +0100 Subject: [PATCH 4/4] Revert "Merge branch 'release/v6.3' into feature/enum" This reverts commit 404672b255549843051ada6c213fb58e163db1bf, reversing changes made to 8aa73e42531a01cadbebb95b4495b0cc1c02c928. --- .../ITS030SharedTable.cs | 57 ------------------- .../Models/MultipleModels.cs | 31 ---------- .../Attributes/StorableAttribute.cs | 8 --- .../Internal/StorageContextQueryNow.cs | 15 +---- .../Serialization/TableEntityDynamic.cs | 10 +--- .../StoargeEntityMapper.cs | 1 - .../StorageContextAttributeMapping.cs | 9 +-- README.md | 53 ----------------- 8 files changed, 3 insertions(+), 181 deletions(-) delete mode 100644 CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs delete 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 deleted file mode 100644 index e360e85..0000000 --- a/CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 07fd04e..0000000 --- a/CoreHelpers.WindowsAzure.Storage.Table.Tests/Models/MultipleModels.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 36ba837..41dd5f4 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Attributes/StorableAttribute.cs @@ -6,17 +6,9 @@ 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 357a7ca..127e257 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs @@ -3,7 +3,6 @@ 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; @@ -95,20 +94,8 @@ private bool MoveNextInternal(bool initialPage) return MoveNextInternal(false); } - var entityMapper = _context.context.GetEntityMapper(); - // set the item - 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; - } + Current = TableEntityDynamic.fromEntity(_inPageEnumerator.Current, _context.context.GetEntityMapper()); // done return true; diff --git a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs index 9dc4f50..68b5515 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/Serialization/TableEntityDynamic.cs @@ -29,20 +29,12 @@ 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 = 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); + IEnumerable objectProperties = model.GetType().GetTypeInfo().GetProperties(); // visit all properties foreach (PropertyInfo property in objectProperties) { - if (property.Name == entityMapper.TypeField) - continue; - if (ShouldSkipProperty(property)) continue; diff --git a/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs b/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs index e3803de..4003157 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StoargeEntityMapper.cs @@ -9,7 +9,6 @@ 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 8ef27ac..995d5fb 100644 --- a/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs +++ b/CoreHelpers.WindowsAzure.Storage.Table/StorageContextAttributeMapping.cs @@ -62,18 +62,12 @@ 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; @@ -117,8 +111,7 @@ public void AddAttributeMapper(Type type, String optionalTablenameOverride) TableName = String.IsNullOrEmpty(optionalTablenameOverride) ? storableAttribute.Tablename : optionalTablenameOverride, PartitionKeyFormat = partitionKeyFormat, RowKeyFormat = rowKeyFormat, - RowKeyEncoding = rowKeyEncoding, - TypeField = typeField, + RowKeyEncoding = rowKeyEncoding }); } diff --git a/README.md b/README.md index 2e43bf9..99ebc39 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ [![Build Status](https://github.com/CoreHelpers/AzureStorageTable/actions/workflows/ci-build.yml/badge.svg)](https://github.com/CoreHelpers/AzureStorageTable/actions/workflows/ci-build.yml) -! NOTICE THIS IS A CUSTOM BUILD OF AzureStorageTable that contains the following additional features: -* Multiple Types in the same table (Base objects) - # AzureStorageTable 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 @@ -162,56 +159,6 @@ 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!