diff --git a/samples/Common.Samples/Fakers.cs b/samples/Common.Samples/Fakers.cs index 0c70ca1..953c123 100644 --- a/samples/Common.Samples/Fakers.cs +++ b/samples/Common.Samples/Fakers.cs @@ -69,7 +69,7 @@ public static Faker CreateFakePerson(string[] accounts = null) //A nullable int? with 80% probability of being null. //The .OrNull extension is in the Bogus.Extensions namespace. - .RuleFor(p => p.OtherAddress, f => FakedAddress().Generate(5)) + .RuleFor(p => p.OtherAddresses, f => FakedAddress().Generate(5)) .RuleFor(p => p.LocalCreated, f => f.Date.Between(DateTime.UtcNow.AddYears(-4), DateTime.UtcNow)) .RuleFor(p => p.LocalUpdated, f => f.Date.Between(DateTime.UtcNow.AddYears(-4), DateTime.UtcNow)) .RuleFor(p => p.Created, (f, a) => new DateTimeOffset(a.LocalCreated.Value)) diff --git a/samples/Common.Samples/Models/AdressType.cs b/samples/Common.Samples/Models/AdressType.cs index 6a03f25..90beac1 100644 --- a/samples/Common.Samples/Models/AdressType.cs +++ b/samples/Common.Samples/Models/AdressType.cs @@ -2,7 +2,7 @@ { public enum AdressType { - Billing, - Home + Billing = 0, + Home = 1 } } \ No newline at end of file diff --git a/samples/Common.Samples/Models/PersonEntity.cs b/samples/Common.Samples/Models/PersonEntity.cs index 2239ae8..f324a59 100644 --- a/samples/Common.Samples/Models/PersonEntity.cs +++ b/samples/Common.Samples/Models/PersonEntity.cs @@ -52,7 +52,7 @@ public class PersonEntity public DateTime LocalUpdated { get; set; } public bool? Enabled { get; set; } public Address Address { get; set; } - public List
OtherAddress { get; set; } + public List
OtherAddresses { get; set; } public Guid PersonId { get; set; } public string FirstName { get; set; } public int? Rank { get; set; } diff --git a/samples/TableClient.Basic.Sample/SampleConsole.cs b/samples/TableClient.Basic.Sample/SampleConsole.cs index 1c5bba4..288e10e 100644 --- a/samples/TableClient.Basic.Sample/SampleConsole.cs +++ b/samples/TableClient.Basic.Sample/SampleConsole.cs @@ -31,8 +31,7 @@ public static async Task Run() config .SetPartitionKey(entity => entity.TenantId) .SetRowKeyProp(entity => entity.PersonId) - .IgnoreProp(entity => entity.OtherAddress) - + .IgnoreProp(entity => entity.OtherAddresses) //add computed props to store and compute dynamically additional fields of the entity .AddComputedProp("_IsInFrance", p => p.Address?.State == "France") .AddComputedProp("_FirstLastName3Chars", p => p.LastName?.ToLower()[..3]); diff --git a/samples/TableClient.DependencyInjection.AdvancedSample/Program.cs b/samples/TableClient.DependencyInjection.AdvancedSample/Program.cs index f5ae931..19a9ec4 100644 --- a/samples/TableClient.DependencyInjection.AdvancedSample/Program.cs +++ b/samples/TableClient.DependencyInjection.AdvancedSample/Program.cs @@ -59,9 +59,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureEntity((sp, config) => config .SetPartitionKey(p => p.TenantId) .SetRowKeyProp(p => p.PersonId) - .IgnoreProp(p => p.OtherAddress) + .IgnoreProp(p => p.OtherAddresses) .AddComputedProp("_IsInFrance", p => p.Address?.State == "France") - .AddComputedProp("_MoreThanOneAddress", p => p.OtherAddress?.Count > 1) + .AddComputedProp("_MoreThanOneAddress", p => p.OtherAddresses?.Count > 1) .AddObserver("LastNameProjection", () => sp.GetService()) ); }); diff --git a/samples/TableClient.DependencyInjection.BasicSample/Program.cs b/samples/TableClient.DependencyInjection.BasicSample/Program.cs index a941c16..7aeb10d 100644 --- a/samples/TableClient.DependencyInjection.BasicSample/Program.cs +++ b/samples/TableClient.DependencyInjection.BasicSample/Program.cs @@ -35,9 +35,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureEntity((sp, config) => config .SetPartitionKey(entity => entity.TenantId) .SetRowKeyProp(entity => entity.PersonId) - .IgnoreProp(entity => entity.OtherAddress) + .IgnoreProp(entity => entity.OtherAddresses) .AddComputedProp("_IsInFrance", entity => entity.Address?.State == "France") - .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddress?.Count > 1) + .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddresses?.Count > 1) ); }); }); diff --git a/samples/TableClient.DependencyInjection.ProjectionSample/Program.cs b/samples/TableClient.DependencyInjection.ProjectionSample/Program.cs index 5fc2e13..b09935c 100644 --- a/samples/TableClient.DependencyInjection.ProjectionSample/Program.cs +++ b/samples/TableClient.DependencyInjection.ProjectionSample/Program.cs @@ -50,9 +50,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .SetPartitionKey(entity => entity.TenantId) .SetRowKeyProp(entity => entity.PersonId) .AddTag(entity => entity.LastName) - .IgnoreProp(entity => entity.OtherAddress) + .IgnoreProp(entity => entity.OtherAddresses) .AddComputedProp("_IsInFrance", entity => entity.Address?.State == "France") - .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddress?.Count > 1) + .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddresses?.Count > 1) )) .WithName("Source"); }); diff --git a/samples/TableClient.Legacy.Sample/Program.cs b/samples/TableClient.Legacy.Sample/Program.cs index b7cbcae..82e5e3c 100644 --- a/samples/TableClient.Legacy.Sample/Program.cs +++ b/samples/TableClient.Legacy.Sample/Program.cs @@ -33,11 +33,10 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureEntity(entityConfig => entityConfig .SetPartitionKey(entity => entity.TenantId) .SetRowKeyProp(entity => entity.PersonId) - - .IgnoreProp(entity => entity.OtherAddress) + .IgnoreProp(entity => entity.OtherAddresses) .AddComputedProp("_IsInFrance", entity => entity.Address?.State == "France") - .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddress?.Count > 1) + .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddresses?.Count > 1) .AddComputedProp("_CreatedNext6Month", entity => entity.Created > DateTimeOffset.UtcNow.AddMonths(-6)) .AddComputedProp("_FirstLastName3Chars", entity => entity.LastName?.ToLower()[..3]) diff --git a/samples/TableClient.Performance.Sample/SampleConsole.cs b/samples/TableClient.Performance.Sample/SampleConsole.cs index 4a714f7..c31c178 100644 --- a/samples/TableClient.Performance.Sample/SampleConsole.cs +++ b/samples/TableClient.Performance.Sample/SampleConsole.cs @@ -32,7 +32,7 @@ public static async Task Run() config .SetPartitionKey(entity => entity.TenantId) .SetRowKeyProp(entity => entity.PersonId) - .IgnoreProp(entity => entity.OtherAddress) + .IgnoreProp(entity => entity.OtherAddresses) //add tag to generate indexed and sorted entities through rowKey .AddTag(entity => entity.Created) @@ -42,7 +42,7 @@ public static async Task Run() //add computed props to store and compute dynamically additional fields of the entity .AddComputedProp("_IsInFrance", entity => entity.Address?.State == "France") - .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddress?.Count > 1) + .AddComputedProp("_MoreThanOneAddress", entity => entity.OtherAddresses?.Count > 1) .AddComputedProp("_CreatedNext6Month", entity => entity.Created > DateTimeOffset.UtcNow.AddMonths(-6)) .AddComputedProp("_FirstLastName3Chars", entity => entity.LastName?.ToLower()[..3]) diff --git a/src/Azure.EntityServices.Tables/Core/EntityValueAdapter.cs b/src/Azure.EntityServices.Tables/Core/EntityValueAdapter.cs index ec4ef6e..1cc0f2b 100644 --- a/src/Azure.EntityServices.Tables/Core/EntityValueAdapter.cs +++ b/src/Azure.EntityServices.Tables/Core/EntityValueAdapter.cs @@ -15,7 +15,11 @@ public static class EntityValueAdapter /// /// /// - public static object WriteValue(object value, PropertyInfo entityProp = null) + public static object WriteValue( + object value, + JsonSerializerOptions serializerOptions, + PropertyInfo entityProp = null + ) { if (value == null) { @@ -49,7 +53,7 @@ public static object WriteValue(object value, PropertyInfo entityProp = null) //otherwise try to serialize in string decimal v => v.ToInvariantString(), float v => v.ToInvariantString(), - _ => JsonSerializer.Serialize(value) + _ => JsonSerializer.Serialize(value, serializerOptions) }; } @@ -59,7 +63,11 @@ public static object WriteValue(object value, PropertyInfo entityProp = null) /// /// /// - public static void ReadValue(T entity, PropertyInfo entityProp, object tablePropValue) + public static void ReadValue( + T entity + , PropertyInfo entityProp, + JsonSerializerOptions serializerOptions, + object tablePropValue = null) { var propertyType = entityProp.PropertyType; @@ -213,10 +221,10 @@ public static void ReadValue(T entity, PropertyInfo entityProp, object tableP if (propertyType.IsClass && !propertyType.IsValueType) { //otherwise it should be a serialized object - entityProp.SetValue(entity, JsonSerializer.Deserialize(strPropValue, propertyType), null); + entityProp.SetValue(entity, JsonSerializer.Deserialize(strPropValue, propertyType, serializerOptions), null); return; } - entityProp.SetValue(entity, JsonSerializer.Deserialize(strPropValue, propertyType), null); + entityProp.SetValue(entity, JsonSerializer.Deserialize(strPropValue, propertyType, serializerOptions), null); return; } diff --git a/src/Azure.EntityServices.Tables/Core/TableEntityBinder.cs b/src/Azure.EntityServices.Tables/Core/TableEntityBinder.cs index 22ed2aa..3191d76 100644 --- a/src/Azure.EntityServices.Tables/Core/TableEntityBinder.cs +++ b/src/Azure.EntityServices.Tables/Core/TableEntityBinder.cs @@ -2,9 +2,9 @@ using Azure.EntityServices.Tables.Extensions; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Reflection; +using System.Text.Json; namespace Azure.EntityServices.Tables.Core { @@ -19,6 +19,8 @@ namespace Azure.EntityServices.Tables.Core public TableEntity TableEntity { get; } + private readonly JsonSerializerOptions _serializerOptions; + public T Entity { get; private set; } public IDictionary Properties { get; } = new Dictionary(); public IDictionary Metadata { get; } = new Dictionary(); @@ -28,61 +30,63 @@ namespace Azure.EntityServices.Tables.Core BindingFlags.Instance | BindingFlags.SetProperty); - private readonly IEnumerable _filteredEntityProperties = EntityProperties.ToList(); private readonly EntityTagBuilder _entityTagBuilder; - private readonly IEnumerable _propsToIgnore= Enumerable.Empty(); + private readonly IEnumerable _propsToIgnore = Enumerable.Empty(); private List FilterEntityProperties() => EntityProperties.Where(p => !_propsToIgnore.Contains(p.Name)).ToList(); - public TableEntityBinder(T entity) + public TableEntityBinder(T entity, JsonSerializerOptions serializerOptions = null) { Entity = entity; TableEntity = new TableEntity(); + _serializerOptions = serializerOptions; } - public TableEntityBinder(T entity, IEnumerable propsToIgnore) + public TableEntityBinder(T entity, IEnumerable propsToIgnore, JsonSerializerOptions serializerOptions = null) { Entity = entity; TableEntity = new TableEntity(); _propsToIgnore = propsToIgnore ?? Enumerable.Empty(); _filteredEntityProperties = FilterEntityProperties(); _propsToIgnore = propsToIgnore.ToList(); + _serializerOptions = serializerOptions; } - public TableEntityBinder(TableEntity tableEntity) + public TableEntityBinder(TableEntity tableEntity, JsonSerializerOptions serializerOptions = null) { TableEntity = tableEntity; + _serializerOptions = serializerOptions; } - public TableEntityBinder(TableEntity tableEntity, IEnumerable propsToIgnore, EntityTagBuilder entityTagBuilder) + public TableEntityBinder(TableEntity tableEntity, IEnumerable propsToIgnore, EntityTagBuilder entityTagBuilder, JsonSerializerOptions serializerOptions = null) { - TableEntity = tableEntity; + TableEntity = tableEntity; _propsToIgnore = propsToIgnore ?? Enumerable.Empty(); _filteredEntityProperties = FilterEntityProperties(); _entityTagBuilder = entityTagBuilder; + _serializerOptions = serializerOptions; } - - public TableEntityBinder(T entity, string partitionKey, string rowKey, IEnumerable propsToIgnore=null, EntityTagBuilder entityTagBuilder=null) + public TableEntityBinder(T entity, string partitionKey, string rowKey, IEnumerable propsToIgnore = null, EntityTagBuilder entityTagBuilder = null, JsonSerializerOptions serializerOptions = null) { Entity = entity; TableEntity = new TableEntity(partitionKey, rowKey); - _propsToIgnore = propsToIgnore ?? Enumerable.Empty() ; - _filteredEntityProperties = FilterEntityProperties(); + _propsToIgnore = propsToIgnore ?? Enumerable.Empty(); + _filteredEntityProperties = FilterEntityProperties(); _entityTagBuilder = entityTagBuilder; + _serializerOptions = serializerOptions; } public TableEntity Bind() { - foreach (var metadata in Metadata) { - TableEntity.AddOrUpdate(metadata.Key, EntityValueAdapter.WriteValue(metadata.Value)); + TableEntity.AddOrUpdate(metadata.Key, EntityValueAdapter.WriteValue(metadata.Value, _serializerOptions)); } if (Entity is TableEntity tbe) { - foreach (var property in tbe.Where(e=> !_propsToIgnore.Contains(e.Key))) + foreach (var property in tbe.Where(e => !_propsToIgnore.Contains(e.Key))) { if (property.Key == "PartitionKey" || property.Key == "RowKey" || @@ -91,20 +95,19 @@ public TableEntity Bind() { continue; } - TableEntity.AddOrUpdate(property.Key,property.Value); + TableEntity.AddOrUpdate(property.Key, property.Value); } } else { foreach (var property in _filteredEntityProperties) { - - TableEntity.AddOrUpdate(property.Name, EntityValueAdapter.WriteValue(property.GetValue(Entity), property)); + TableEntity.AddOrUpdate(property.Name, EntityValueAdapter.WriteValue(property.GetValue(Entity), _serializerOptions, property)); } } foreach (var property in Properties) { - TableEntity.AddOrUpdate(property.Key, EntityValueAdapter.WriteValue(property.Value)); + TableEntity.AddOrUpdate(property.Key, EntityValueAdapter.WriteValue(property.Value, _serializerOptions)); } return TableEntity; } @@ -128,23 +131,21 @@ public T UnBind() { foreach (var property in TableEntity.Keys) { - tbe.AddOrUpdate(property, TableEntity[property]); } - } - else - foreach (var property in _filteredEntityProperties) - { - if (TableEntity.TryGetValue(property.Name, out var tablePropValue)) + else + foreach (var property in _filteredEntityProperties) { - EntityValueAdapter.ReadValue(Entity, property, tablePropValue); + if (TableEntity.TryGetValue(property.Name, out var tablePropValue)) + { + EntityValueAdapter.ReadValue(Entity, property, _serializerOptions, tablePropValue); + } } - } return Entity; } - + public void BindDynamicProps(IDictionary> props, bool toDelete = false) { foreach (var prop in props) @@ -159,15 +160,14 @@ public void BindDynamicProps(IDictionary> props, bool to } public void BindTags(Dictionary tags, IList computedTags) - { - + { foreach (var propInfo in tags) { Metadata.AddOrUpdate(_entityTagBuilder.CreateTagName(propInfo.Key), _entityTagBuilder.CreateTagRowKey(propInfo.Value, Entity)); } foreach (var tagPrefix in computedTags) { - Metadata.AddOrUpdate(_entityTagBuilder.CreateTagName(tagPrefix), _entityTagBuilder.CreateTagRowKey(tagPrefix, Metadata[$"{tagPrefix}"], Entity)); + Metadata.AddOrUpdate(_entityTagBuilder.CreateTagName(tagPrefix), _entityTagBuilder.CreateTagRowKey(tagPrefix, Metadata[$"{tagPrefix}"], Entity)); } } } diff --git a/src/Azure.EntityServices.Tables/EntityTableClient.cs b/src/Azure.EntityServices.Tables/EntityTableClient.cs index 2f2262f..7837157 100644 --- a/src/Azure.EntityServices.Tables/EntityTableClient.cs +++ b/src/Azure.EntityServices.Tables/EntityTableClient.cs @@ -45,10 +45,10 @@ public class EntityTableClient : IEntityTableClient private EntityTagBuilder _entityTagBuilder; private IEntityBinder CreatePrimaryEntityBinderFromEntity(T entity) - => new TableEntityBinder(entity, ResolvePartitionKey(entity), ResolvePrimaryKey(entity), _config.IgnoredProps, _entityTagBuilder); + => new TableEntityBinder(entity, ResolvePartitionKey(entity), ResolvePrimaryKey(entity), _config.IgnoredProps, _entityTagBuilder, _options.SerializerOptions); private IEntityBinder CreateEntityBinderFromTableEntity(TableEntity tableEntity) - => new TableEntityBinder(tableEntity, _config.IgnoredProps, _entityTagBuilder); + => new TableEntityBinder(tableEntity, _config.IgnoredProps, _entityTagBuilder, _options.SerializerOptions); private TableBatchClient CreateTableBatchClient() { diff --git a/src/Azure.EntityServices.Tables/EntityTableClientOptions.cs b/src/Azure.EntityServices.Tables/EntityTableClientOptions.cs index d8273ac..d546554 100644 --- a/src/Azure.EntityServices.Tables/EntityTableClientOptions.cs +++ b/src/Azure.EntityServices.Tables/EntityTableClientOptions.cs @@ -1,4 +1,6 @@ -namespace Azure.EntityServices.Tables +using System.Text.Json; + +namespace Azure.EntityServices.Tables { public class EntityTableClientOptions { @@ -12,7 +14,8 @@ public EntityTableClientOptions( int maxItemToGroup = 1000, int maxItemInTransaction = 100, bool createTableIfNotExists = false, - bool handleTagMutation = false + bool handleTagMutation = false, + JsonSerializerOptions serializerOptions = default ) { TableName = tableName; @@ -21,6 +24,7 @@ public EntityTableClientOptions( MaxItemToGroup = maxItemToGroup; CreateTableIfNotExists = createTableIfNotExists; HandleTagMutation = handleTagMutation; + SerializerOptions = serializerOptions ?? new JsonSerializerOptions(); } public bool CreateTableIfNotExists { get; set; } @@ -29,5 +33,6 @@ public EntityTableClientOptions( public int MaxOperationPerTransaction { get; set; } = 100; public int MaxItemToGroup { get; set; } = 1000; public bool HandleTagMutation { get; set; } + public JsonSerializerOptions SerializerOptions { get; set; } = new(); } } \ No newline at end of file diff --git a/src/Azure.EntityServices.Tables/Extensions/EntityTableclientOptionExtensions.cs b/src/Azure.EntityServices.Tables/Extensions/EntityTableclientOptionExtensions.cs new file mode 100644 index 0000000..13d8aa0 --- /dev/null +++ b/src/Azure.EntityServices.Tables/Extensions/EntityTableclientOptionExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Text.Json; + +namespace Azure.EntityServices.Tables +{ + public static class EntityTableclientOptionExtensions + { + public static EntityTableClientOptions ConfigureSerializer(this EntityTableClientOptions options,Action serializerConfigurator) + { + serializerConfigurator?.Invoke(options.SerializerOptions); + return options; + } + + public static EntityTableClientOptions ConfigureSerializerWithStringEnumConverter(this EntityTableClientOptions options) + { + options?.SerializerOptions?.AddJsonStringEnumConverter(); + return options; + } + } +} \ No newline at end of file diff --git a/src/Azure.EntityServices.Tables/Extensions/JSonSerializerOptionsExtensions.cs b/src/Azure.EntityServices.Tables/Extensions/JSonSerializerOptionsExtensions.cs new file mode 100644 index 0000000..4965991 --- /dev/null +++ b/src/Azure.EntityServices.Tables/Extensions/JSonSerializerOptionsExtensions.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Azure.EntityServices.Tables +{ + public static class JSonSerializerOptionsExtensions + { + public static JsonSerializerOptions UseNamingPolicy(this JsonSerializerOptions serializerOptions, JsonNamingPolicy jsonNamingPolicy) + { + serializerOptions.PropertyNamingPolicy = jsonNamingPolicy; + + return serializerOptions; + } + public static JsonSerializerOptions UseCamelCaseNamingPolicy(this JsonSerializerOptions serializerOptions) + { + serializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + + return serializerOptions; + } + public static JsonSerializerOptions AddConverter(this JsonSerializerOptions serializerOptions, JsonConverter jsonConverter) + { + serializerOptions.Converters.Add(jsonConverter); + + return serializerOptions; + } + public static JsonSerializerOptions AddJsonStringEnumConverter(this JsonSerializerOptions serializerOptions) + { + serializerOptions.Converters.Add(new JsonStringEnumConverter()); + return serializerOptions; + } + } +} \ No newline at end of file diff --git a/tests/Azure.EntityServices.Tests/Table/EntityTableClientTests.cs b/tests/Azure.EntityServices.Tests/Table/EntityTableClientTests.cs index 28b7ef7..423cf99 100644 --- a/tests/Azure.EntityServices.Tests/Table/EntityTableClientTests.cs +++ b/tests/Azure.EntityServices.Tests/Table/EntityTableClientTests.cs @@ -1,4 +1,5 @@ -using Azure.EntityServices.Queries; +using Azure.Data.Tables; +using Azure.EntityServices.Queries; using Azure.EntityServices.Tables; using Azure.EntityServices.Tables.Extensions; using Common.Samples; @@ -8,6 +9,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Azure.EntityServices.Table.Tests @@ -15,15 +18,15 @@ namespace Azure.EntityServices.Table.Tests [TestClass] public class EntityTableClientTests { - private readonly Func _commonOptions; + private readonly Action _commonOptions; public EntityTableClientTests() { - _commonOptions = () => new EntityTableClientOptions() - { - CreateTableIfNotExists = true, - TableName = $"{nameof(EntityTableClientTests)}{Guid.NewGuid():N}", - HandleTagMutation = true + _commonOptions = (EntityTableClientOptions options) => + { + options.CreateTableIfNotExists = true; + options.TableName = $"{nameof(EntityTableClientTests)}{Guid.NewGuid():N}"; + options.HandleTagMutation = true; }; } @@ -34,7 +37,7 @@ public async Task Should_InsertOrReplace_Entity() var person = persons.First(); var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString) - .Configure(_commonOptions(), c => + .Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -55,7 +58,7 @@ public async Task Should_Refresh_Tag_When_Value_Updated() var person = persons.First(); var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString) - .Configure(_commonOptions(), c => + .Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -93,14 +96,16 @@ public async Task Should_InsertOrReplace_Entity_With_Null_values() var person = persons.First(); person.Altitude = null; var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString) - .Configure(_commonOptions(), c => - { - c. - SetPartitionKey(p => p.TenantId) - .SetRowKeyProp(p => p.PersonId) - .AddTag(p => p.LastName) - .AddTag(p => p.Created); - }); + .Configure( + options => _commonOptions(options), + config => + { + config. + SetPartitionKey(p => p.TenantId) + .SetRowKeyProp(p => p.PersonId) + .AddTag(p => p.LastName) + .AddTag(p => p.Created); + }); await entityTable.AddOrReplaceAsync(person); var created = await entityTable.GetByIdAsync(person.TenantId, person.PersonId); @@ -114,7 +119,7 @@ public async Task Should_Ignore_Entity_Prop() var persons = Fakers.CreateFakePerson().Generate(1); var person = persons.First(); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -136,7 +141,7 @@ public async Task Should_Get_By_Indexed_Tag_With_Filter() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -174,7 +179,7 @@ public async Task Should_Get_By_Indexed_Tag_Without_Given_Partition_Key() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -211,7 +216,7 @@ public async Task Should_Get_By_Indexed_Tag_Without_Given_Partition_Key() public async Task Should_Set_Primary_Key_On_InsertOrUpdate() { var person = Fakers.CreateFakePerson().Generate(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId); c.SetRowKeyProp(p => p.PersonId); @@ -233,7 +238,7 @@ public async Task Should_Set_Primary_Key_On_InsertOrUpdate() public async Task Should_Get_Indexed_Tag_After_InsertOrUpdate() { var person = Fakers.CreateFakePerson().Generate(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId); c.SetRowKeyProp(p => p.PersonId); @@ -264,7 +269,7 @@ public async Task Should_Set_Dynamic_Prop_On_InsertOrUpdate() static string First3Char(string s) => s.ToLower()[..3]; var person = Fakers.CreateFakePerson().Generate(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId); c.SetRowKeyProp(p => p.PersonId); @@ -288,7 +293,7 @@ public async Task Should_Set_Computed_Index_On_InsertOrUpdate() { static string First3Char(string s) => s.ToLower()[..3]; var person = Fakers.CreateFakePerson().Generate(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId); c.SetRowKeyProp(p => p.PersonId); @@ -322,7 +327,7 @@ public async Task Should_Remove_Tags_OnDelete() var person = Fakers.CreateFakePerson().Generate(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId); c.SetRowKeyProp(p => p.PersonId); @@ -370,12 +375,12 @@ public async Task Should_Observe_Entity_Table_Updates() var persons = Fakers.CreateFakePerson().Generate(10); var observer = new DummyObserver(); - var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c.SetPartitionKey(p => p.TenantId) .SetRowKeyProp(p => p.PersonId) .AddTag(p => p.LastName) - .AddObserver(nameof(DummyObserver),()=> observer); + .AddObserver(nameof(DummyObserver), () => observer); }); try { @@ -403,7 +408,7 @@ public async Task Should_Insert_Many_Indexed_Entities() var partitionName = Guid.NewGuid().ToString(); persons.ForEach(p => p.TenantId = partitionName); - IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), config => + IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), config => { config .SetPartitionKey(p => p.TenantId) @@ -440,7 +445,7 @@ public async Task Should_Update_Many_Indexed_Entities() var partitionName = Guid.NewGuid().ToString(); persons.ForEach(p => p.TenantId = partitionName); - IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), config => + IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), config => { config .SetPartitionKey(p => p.TenantId) @@ -484,7 +489,7 @@ public async Task Should_Update_Many_Indexed_Entities_With_No_Existing_Entities( var partitionName = Guid.NewGuid().ToString(); persons.ForEach(p => p.TenantId = partitionName); - IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), config => + IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), config => { config .SetPartitionKey(p => p.TenantId) @@ -517,7 +522,7 @@ public async Task Should_Filter_Entities() var partitionName = Guid.NewGuid().ToString(); persons.ForEach(p => p.TenantId = partitionName); - IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), config => + IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), config => { config .SetPartitionKey(p => p.TenantId) @@ -559,7 +564,7 @@ public async Task Should_Filter_Entities_With_Nullable_Properties() var partitionName = Guid.NewGuid().ToString(); persons.ForEach(p => p.TenantId = partitionName); - IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), config => + IEntityTableClient tableEntity = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), config => { config .SetPartitionKey(p => p.TenantId) @@ -606,7 +611,7 @@ public async Task Should_Query_By_Tag_Filter_With_Equal_Extension() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -640,7 +645,7 @@ public async Task Should_Query_By_Tag_Filter_With_GreaterThanOrEqual() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -673,7 +678,7 @@ public async Task Should_Query_By_Tag_Filter_With_GreaterThan() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -709,7 +714,7 @@ public async Task Should_Query_By_Tag_Filter_With_LessThan() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -746,7 +751,7 @@ public async Task Should_Query_By_Tag_Filter_With_LessThanOrEqual() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -782,7 +787,7 @@ public async Task Should_Query_By_Tag_Filter_With_Between_Extension() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -827,7 +832,7 @@ public async Task Should_Combine_Query_With_Mixed_Tag_And_Prop_Filters() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -868,7 +873,7 @@ public async Task Should_Query_By_Tag_Filter_With_Nullable_Values() { var persons = Fakers.CreateFakePerson().Generate(10); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -903,7 +908,7 @@ public async Task Should_Query_By_Tag_Filter_With_Empty_Values() { var persons = Fakers.CreateFakePerson().Generate(1); - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c. SetPartitionKey(p => p.TenantId) @@ -942,7 +947,7 @@ public async Task Should_Store_Default_DateTime_Values() person.LocalCreated = default; person.LocalUpdated = default; - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c .SetPartitionKey(p => p.TenantId) @@ -971,7 +976,7 @@ public async Task Should_Store_Null_DateTime_Values() person.Created = null; person.LocalCreated = null; - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c .SetPartitionKey(p => p.TenantId) @@ -1001,7 +1006,7 @@ public async Task Should_Escape_Not_Supported_Char_For_PartitionKeys() person.Created = null; person.LocalCreated = null; - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c .SetPartitionKey(p => p.TenantId) @@ -1034,7 +1039,7 @@ public async Task Should_Escape_Not_Supported_Char_For_RowKeys() person.Created = null; person.LocalCreated = null; - var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(_commonOptions(), c => + var entityTable = EntityTableClient.Create(TestEnvironment.ConnectionString).Configure(options => _commonOptions(options), c => { c .SetPartitionKey(p => p.TenantId) @@ -1062,5 +1067,103 @@ public async Task Should_Escape_Not_Supported_Char_For_RowKeys() await entityTable.DropTableAsync(); } } + + [TestMethod] + public async Task Should_Serialize_Enum_As_String() + { + var person = Fakers.CreateFakePerson().Generate(1).FirstOrDefault(); + var options = new EntityTableClientOptions() { }; + _commonOptions.Invoke(options); + options.ConfigureSerializerWithStringEnumConverter(); + + var genericClient = EntityTableClient.Create(TestEnvironment.ConnectionString) + .Configure(options, c => + { + c + .SetPartitionKey(p => p.PartitionKey) + .SetRowKeyProp(p => p.RowKey); + }); + + var personClient = EntityTableClient.Create(TestEnvironment.ConnectionString) + .Configure(options, + c => + { + c + .SetPartitionKey(p => p.TenantId) + .SetRowKeyProp(p => p.LastName) + .AddTag(p => p.LastName) + .AddTag(p => p.Created); + }); + try + { + await personClient.AddOrReplaceAsync(person); + + var genericEntity = await genericClient.GetByIdAsync(person.TenantId, person.LastName); + var serializedAddressIncludingEnum = JsonSerializer.Serialize(person.Address, new JsonSerializerOptions + { + Converters = { + new JsonStringEnumConverter() + } + }); + genericEntity.GetString("Address")?.Should().Be(serializedAddressIncludingEnum); + + var updatedEntity = await personClient.GetByIdAsync(person.TenantId, person.LastName); + updatedEntity.Address.Should().BeEquivalentTo(person.Address); + } + catch { throw; } + finally + { + await personClient.DropTableAsync(); + } + } + + [TestMethod] + public async Task Should_DeSerialize_Existing_Enum_As_Integer() + { + var person = Fakers.CreateFakePerson().Generate(1).FirstOrDefault(); + var options = new EntityTableClientOptions() { }; + _commonOptions.Invoke(options); + + var genericClient = EntityTableClient.Create(TestEnvironment.ConnectionString) + .Configure(options, c => + { + c + .SetPartitionKey(p => p.PartitionKey) + .SetRowKeyProp(p => p.RowKey); + }); + + var personClient = EntityTableClient.Create(TestEnvironment.ConnectionString) + .Configure(options, + c => + { + c + .SetPartitionKey(p => p.TenantId) + .SetRowKeyProp(p => p.LastName) + .AddTag(p => p.LastName) + .AddTag(p => p.Created); + }); + try + { + await personClient.AddOrReplaceAsync(person); + + var genericEntity = await genericClient.GetByIdAsync(person.TenantId, person.LastName); + + var serializedAddress = JsonSerializer.Serialize(person.Address); + + genericEntity.GetString("Address")?.Should().Be( + serializedAddress, + because: "By default, AdressType will be serialized as integer"); + + // deserializer should map json integer value with related enum value + var updatedEntity = await personClient.GetByIdAsync(person.TenantId, person.LastName); + updatedEntity.Address.Should() + .BeEquivalentTo(person.Address); + } + catch { throw; } + finally + { + await personClient.DropTableAsync(); + } + } } } \ No newline at end of file