From cd162551e697910d266ca2632d189672e5f5daf3 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 24 Oct 2025 16:22:27 -0230 Subject: [PATCH 01/10] clean up --- .gitignore | 8 +- .../CustomAutoAdapterMapper.Tests.csproj | 36 +- .../PublicAPIViewModel.cs | 11 +- .../PublicAPIViewModelWithVariation.cs | 11 +- CustomAutoAdapterMapper.Tests/TestObject.cs | 27 +- .../TestObjectWithVariation.cs | 27 +- CustomAutoAdapterMapper.Tests/UnitTest1.cs | 385 +++++++++--------- .../CustomAutoAdapterMapper.csproj | 53 +-- ...stomAutoAdapterMapper.sln.DotSettings.user | 4 + .../Exceptions/ItemKeyOptionNullException.cs | 5 +- .../Exceptions/JsonContentException.cs | 5 +- .../Exceptions/RootKeyOptionNullException.cs | 5 +- .../RootKeyPropertyNullException.cs | 5 +- CustomAutoAdapterMapper/Mapper.cs | 78 ++-- CustomAutoAdapterMapper/Option.cs | 18 +- README.md | 22 +- 16 files changed, 329 insertions(+), 371 deletions(-) create mode 100644 CustomAutoAdapterMapper/CustomAutoAdapterMapper.sln.DotSettings.user diff --git a/.gitignore b/.gitignore index 3bdef59..cc152ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ -CustomAutoAdapterMapper/obj/ +obj CustomAutoAdapterMapper/.vs/ -CustomAutoAdapterMapper.Tests/bin/ -CustomAutoAdapterMapper.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs -CustomAutoAdapterMapper.Tests/obj/ -CustomAutoAdapterMapper/bin/ +bin/ +.idea \ No newline at end of file diff --git a/CustomAutoAdapterMapper.Tests/CustomAutoAdapterMapper.Tests.csproj b/CustomAutoAdapterMapper.Tests/CustomAutoAdapterMapper.Tests.csproj index a8d1e9a..5e9748a 100644 --- a/CustomAutoAdapterMapper.Tests/CustomAutoAdapterMapper.Tests.csproj +++ b/CustomAutoAdapterMapper.Tests/CustomAutoAdapterMapper.Tests.csproj @@ -1,25 +1,25 @@ - - net8.0 - enable - enable + + net8.0 + enable + enable - false - true - + false + true + - - - - - - - - + + + + + + + + - - - + + + diff --git a/CustomAutoAdapterMapper.Tests/PublicAPIViewModel.cs b/CustomAutoAdapterMapper.Tests/PublicAPIViewModel.cs index 7d6e2f9..ab8c309 100644 --- a/CustomAutoAdapterMapper.Tests/PublicAPIViewModel.cs +++ b/CustomAutoAdapterMapper.Tests/PublicAPIViewModel.cs @@ -1,8 +1,7 @@ -namespace CustomAutoAdapterMapper.Tests +namespace CustomAutoAdapterMapper.Tests; + +public class PublicApiViewModel { - public class PublicAPIViewModel - { - public int Count { get; set; } - public List Entries { get; set; } - } + public int Count { get; set; } + public List Entries { get; set; } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper.Tests/PublicAPIViewModelWithVariation.cs b/CustomAutoAdapterMapper.Tests/PublicAPIViewModelWithVariation.cs index 9913ef7..1f77a7c 100644 --- a/CustomAutoAdapterMapper.Tests/PublicAPIViewModelWithVariation.cs +++ b/CustomAutoAdapterMapper.Tests/PublicAPIViewModelWithVariation.cs @@ -1,8 +1,7 @@ -namespace CustomAutoAdapterMapper.Tests +namespace CustomAutoAdapterMapper.Tests; + +public class PublicApiViewModelWithVariation { - public class PublicAPIViewModelWithVariation - { - public int Count { get; set; } - public List Entries { get; set; } - } + public int Count { get; set; } + public List Entries { get; set; } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper.Tests/TestObject.cs b/CustomAutoAdapterMapper.Tests/TestObject.cs index 9d88004..121c098 100644 --- a/CustomAutoAdapterMapper.Tests/TestObject.cs +++ b/CustomAutoAdapterMapper.Tests/TestObject.cs @@ -1,16 +1,15 @@ -namespace CustomAutoAdapterMapper.Tests +namespace CustomAutoAdapterMapper.Tests; + +public class TestObject { - public class TestObject - { - public string API { get; set; } - public string Description { get; set; } - public string Auth { get; set; } - public string HTTPS { get; set; } - public string Cors { get; set; } - public string Link { get; set; } - public string Category { get; set; } - public string ReportsToEmail { get; set; } - public int ReportsToIdInCompany { get; set; } - public int EmployeeIdInCompany { get; set; } - } + public string API { get; set; } + public string Description { get; set; } + public string Auth { get; set; } + public string HTTPS { get; set; } + public string Cors { get; set; } + public string Link { get; set; } + public string Category { get; set; } + public string ReportsToEmail { get; set; } + public int ReportsToIdInCompany { get; set; } + public int EmployeeIdInCompany { get; set; } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper.Tests/TestObjectWithVariation.cs b/CustomAutoAdapterMapper.Tests/TestObjectWithVariation.cs index 524f112..367f5a9 100644 --- a/CustomAutoAdapterMapper.Tests/TestObjectWithVariation.cs +++ b/CustomAutoAdapterMapper.Tests/TestObjectWithVariation.cs @@ -1,16 +1,15 @@ -namespace CustomAutoAdapterMapper.Tests +namespace CustomAutoAdapterMapper.Tests; + +public class TestObjectWithVariation { - public class TestObjectWithVariation - { - public string API { get; set; } - public string DescriptionVariation { get; set; } - public string AuthVariation { get; set; } - public string HTTPS { get; set; } - public string Cors { get; set; } - public string Link { get; set; } - public string CategoryVariation { get; set; } - public string ReportsToEmailVariation { get; set; } - public int ReportsToIdInCompanyVariation { get; set; } - public int EmployeeIdInCompanyVariation { get; set; } - } + public string API { get; set; } + public string DescriptionVariation { get; set; } + public string AuthVariation { get; set; } + public string HTTPS { get; set; } + public string Cors { get; set; } + public string Link { get; set; } + public string CategoryVariation { get; set; } + public string ReportsToEmailVariation { get; set; } + public int ReportsToIdInCompanyVariation { get; set; } + public int EmployeeIdInCompanyVariation { get; set; } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper.Tests/UnitTest1.cs b/CustomAutoAdapterMapper.Tests/UnitTest1.cs index 657af8c..fb42cb5 100644 --- a/CustomAutoAdapterMapper.Tests/UnitTest1.cs +++ b/CustomAutoAdapterMapper.Tests/UnitTest1.cs @@ -1,32 +1,29 @@ using AutoFixture; using CustomAutoAdapterMapper.Exceptions; using Newtonsoft.Json; -using System.Net.Http.Json; -namespace CustomAutoAdapterMapper.Tests +namespace CustomAutoAdapterMapper.Tests; + +public class Tests { - public class Tests + private string _result; + + [SetUp] + public void Setup() { - private string _result; - [SetUp] - public void Setup() - { - var client = new HttpClient(); - var result = client.GetAsync("https://api.publicapis.org/entries").Result; - _result = result.Content.ReadAsStringAsync().Result; - if (string.IsNullOrEmpty(_result)) - { - _result = Fallback(); - } - } + var client = new HttpClient(); + var result = client.GetAsync("https://api.publicapis.org/entries").Result; + _result = result.Content.ReadAsStringAsync().Result; + if (string.IsNullOrEmpty(_result)) _result = Fallback(); + } - private string Fallback() - { - return $@" - {{ + private string Fallback() + { + return @" + { 'count': 1427, 'entries': [ - {{ + { 'API': 'AdoptAPet', 'Description': 'Resource to help get pets adopted', 'Auth': 'apiKey', @@ -34,8 +31,8 @@ private string Fallback() 'Cors': 'yes', 'Link': 'https://www.adoptapet.com/public/apis/pet_list.html', 'Category': 'Animals' - }}, - {{ + }, + { 'API': 'Axolotl', 'Description': 'Collection of axolotl pictures and facts', 'Auth': '', @@ -43,232 +40,224 @@ private string Fallback() 'Cors': 'no', 'Link': 'https://theaxolotlapi.netlify.app/', 'Category': 'Animals' - }} + } ] - }} + } "; - } + } - [Test] - public void TestMapperThrowsExceptionWhenRootKeyOptionIsNotPassed() - { - var endpointResult = JsonConvert.SerializeObject(new { Test = "ABCDE" }); - var destinationCollection = new List(); - Assert.Throws(() => endpointResult.MapCollection(destinationCollection, null)); - } + [Test] + public void TestMapperThrowsExceptionWhenRootKeyOptionIsNotPassed() + { + var endpointResult = JsonConvert.SerializeObject(new { Test = "ABCDE" }); + var destinationCollection = new List(); + Assert.Throws(() => endpointResult.MapCollection(destinationCollection, null)); + } - [Test] - public void TestMapperThrowsExceptionWhenJsonStringIsInvalid() - { - var endpointResult = ""; - var destinationCollection = new List(); - Assert.Throws(() => endpointResult.MapCollection(destinationCollection, null)); - } + [Test] + public void TestMapperThrowsExceptionWhenJsonStringIsInvalid() + { + var endpointResult = ""; + var destinationCollection = new List(); + Assert.Throws(() => endpointResult.MapCollection(destinationCollection, null)); + } - [Test] - public void TestMapperThrowsExceptionWhenEndpointResultCannotBePassedToAnObject() - { - var endpointResult = "[1,2,3,4,5]"; - var destinationCollection = new List(); + [Test] + public void TestMapperThrowsExceptionWhenEndpointResultCannotBePassedToAnObject() + { + var endpointResult = "[1,2,3,4,5]"; + var destinationCollection = new List(); - Assert.Throws(() => endpointResult.MapCollection(destinationCollection, options => - { - options.RootKey = "SOMETHING"; - })); - } + Assert.Throws(() => + endpointResult.MapCollection(destinationCollection, options => { options.RootKey = "SOMETHING"; })); + } - [Test] - public void TestMapperShouldReturnEmptyWhenEndpointResultIsEmptyOrNull() - { - var endpointResult = JsonConvert.SerializeObject(new { Name = "123", State = "Arizona" }); - var destinationCollection = new List(); + [Test] + public void TestMapperShouldReturnEmptyWhenEndpointResultIsEmptyOrNull() + { + var endpointResult = JsonConvert.SerializeObject(new { Name = "123", State = "Arizona" }); + var destinationCollection = new List(); + + Assert.Throws(() => + endpointResult.MapCollection(destinationCollection, options => { options.RootKey = "SOMETHING"; })); + } - Assert.Throws(() => endpointResult.MapCollection(destinationCollection, options => + [Test] + public void TestMapperReturnsCollectionWhenRootKeyIsCorrect() + { + var destinationCollection = new List(); + var result = _result.MapCollection(destinationCollection, options => { options.RootKey = "entries"; }); + Assert.That(result, Is.EqualTo(destinationCollection)); + } + + [Test] + public void TestMapperReturnsCollectionWithMappingConfigurationCorrect() + { + var destinationCollection = new List(); + var result = _result.MapCollection(destinationCollection, options => + { + options.RootKey = "entries"; + options.Mappings = new Dictionary { - options.RootKey = "SOMETHING"; - })); - } + { "DescriptionVariation", "Description" }, + { "AuthVariation", "Auth" }, + { "CategoryVariation", "Category" } + }; + }); + + var firstItem = result.FirstOrDefault(); - [Test] - public void TestMapperReturnsCollectionWhenRootKeyIsCorrect() + Assert.That(result, Is.EqualTo(destinationCollection)); + Assert.That(firstItem.DescriptionVariation, Is.Not.Null); + } + + [Test] + public void TestMapperThrowsExceptionWhenCollectionIsNotEmptyAndItemKeyIsNeeded() + { + var client = new HttpClient(); + var publicApiResult = JsonConvert.DeserializeObject(_result); + + var destinationCollection = publicApiResult.Entries; + + Assert.Throws(() => _result.MapCollection(destinationCollection, options => { - var destinationCollection = new List(); - var result = _result.MapCollection(destinationCollection, options => + options.RootKey = "entries"; + options.Mappings = new Dictionary { - options.RootKey = "entries"; - }); - Assert.That(result, Is.EqualTo(destinationCollection)); - } + { "DescriptionVariation", "Description" }, + { "AuthVariation", "Auth" }, + { "CategoryVariation", "Category" } + }; + })); + } + + [Test] + public void TestMapperReturnsCorrectCollectionWithMappingConfigurationWithAnEmptyCollectionSupplied() + { + var client = new HttpClient(); + var publicApiResult = JsonConvert.DeserializeObject(_result); - [Test] - public void TestMapperReturnsCollectionWithMappingConfigurationCorrect() + var destinationCollection = new List(); + var result = _result.MapCollection(destinationCollection, options => { - var destinationCollection = new List(); - var result = _result.MapCollection(destinationCollection, options => + options.RootKey = "entries"; + options.Mappings = new Dictionary { - options.RootKey = "entries"; - options.Mappings = new Dictionary - { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "CategoryVariation", "Category" }, - }; - }); + { "DescriptionVariation", "Description" }, + { "AuthVariation", "Auth" }, + { "CategoryVariation", "Category" } + }; + }); - var firstItem = result.FirstOrDefault(); + var firstItemFromOriginal = publicApiResult.Entries.FirstOrDefault(); + var firstItem = result.FirstOrDefault(); - Assert.That(result, Is.EqualTo(destinationCollection)); - Assert.That(firstItem.DescriptionVariation, Is.Not.Null); - } + Assert.That(firstItemFromOriginal, Is.Not.Null); + Assert.That(firstItem.DescriptionVariation, Is.Not.Null); - [Test] - public void TestMapperThrowsExceptionWhenCollectionIsNotEmptyAndItemKeyIsNeeded() - { - var client = new HttpClient(); - var publicAPIResult = JsonConvert.DeserializeObject(_result); + Assert.That(firstItemFromOriginal.Description, Is.EqualTo(firstItem.DescriptionVariation)); + Assert.That(firstItemFromOriginal.Auth, Is.EqualTo(firstItem.AuthVariation)); + Assert.That(firstItemFromOriginal.Category, Is.EqualTo(firstItem.CategoryVariation)); + Assert.That(result, Is.EqualTo(destinationCollection)); + } - var destinationCollection = publicAPIResult.Entries; + [Test] + public void TestMapperReturnsCorrectCollectionWithMappingConfigurationWithCollection() + { + var client = new HttpClient(); + var publicApiResult = JsonConvert.DeserializeObject(_result); - Assert.Throws(() => _result.MapCollection(destinationCollection, options => - { - options.RootKey = "entries"; - options.Mappings = new Dictionary - { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "CategoryVariation", "Category" }, - }; - })); - } - - [Test] - public void TestMapperReturnsCorrectCollectionWithMappingConfigurationWithAnEmptyCollectionSupplied() - { - var client = new HttpClient(); - var publicAPIResult = JsonConvert.DeserializeObject(_result); + var fixture = new Fixture(); + var destinationCollection = JsonConvert.DeserializeObject(_result); - var destinationCollection = new List(); - var result = _result.MapCollection(destinationCollection, options => - { - options.RootKey = "entries"; - options.Mappings = new Dictionary - { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "CategoryVariation", "Category" }, - }; - }); - - var firstItemFromOriginal = publicAPIResult.Entries.FirstOrDefault(); - var firstItem = result.FirstOrDefault(); - - Assert.That(firstItemFromOriginal, Is.Not.Null); - Assert.That(firstItem.DescriptionVariation, Is.Not.Null); - - Assert.That(firstItemFromOriginal.Description, Is.EqualTo(firstItem.DescriptionVariation)); - Assert.That(firstItemFromOriginal.Auth, Is.EqualTo(firstItem.AuthVariation)); - Assert.That(firstItemFromOriginal.Category, Is.EqualTo(firstItem.CategoryVariation)); - Assert.That(result, Is.EqualTo(destinationCollection)); - } - - [Test] - public void TestMapperReturnsCorrectCollectionWithMappingConfigurationWithCollection() + var result = _result.MapCollection(destinationCollection.Entries, options => { - var client = new HttpClient(); - var publicAPIResult = JsonConvert.DeserializeObject(_result); + options.RootKey = "entries"; + options.ItemKey = "API"; + options.Mappings = new Dictionary + { + { "DescriptionVariation", "Description" }, + { "AuthVariation", "Auth" }, + { "CategoryVariation", "Category" } + }; + }); - var fixture = new Fixture(); - var destinationCollection = JsonConvert.DeserializeObject(_result); + var firstItemFromOriginal = publicApiResult.Entries.FirstOrDefault(); + var firstItem = result.FirstOrDefault(); - var result = _result.MapCollection(destinationCollection.Entries, options => - { - options.RootKey = "entries"; - options.ItemKey = "API"; - options.Mappings = new Dictionary + Assert.That(firstItemFromOriginal, Is.Not.Null); + Assert.That(firstItem.DescriptionVariation, Is.Not.Null); + + Assert.That(firstItemFromOriginal.Description, Is.EqualTo(firstItem.DescriptionVariation)); + Assert.That(firstItemFromOriginal.Auth, Is.EqualTo(firstItem.AuthVariation)); + Assert.That(firstItemFromOriginal.Category, Is.EqualTo(firstItem.CategoryVariation)); + Assert.That(result, Is.EqualTo(destinationCollection.Entries)); + } + + private string SampleCollectionWithNestedObjectsAsProperties() + { + return @" { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "CategoryVariation", "Category" }, - }; - }); - - var firstItemFromOriginal = publicAPIResult.Entries.FirstOrDefault(); - var firstItem = result.FirstOrDefault(); - - Assert.That(firstItemFromOriginal, Is.Not.Null); - Assert.That(firstItem.DescriptionVariation, Is.Not.Null); - - Assert.That(firstItemFromOriginal.Description, Is.EqualTo(firstItem.DescriptionVariation)); - Assert.That(firstItemFromOriginal.Auth, Is.EqualTo(firstItem.AuthVariation)); - Assert.That(firstItemFromOriginal.Category, Is.EqualTo(firstItem.CategoryVariation)); - Assert.That(result, Is.EqualTo(destinationCollection.Entries)); - } - - private string SampleCollectionWithNestedObjectsAsProperties() - { - return $@" - {{ 'count': 1427, 'entries': [ - {{ + { 'API': 'AdoptAPet', 'Description': 'Resource to help get pets adopted', 'Auth': 'apiKey', 'HTTPS': true, 'Cors': 'yes', - 'work': {{ + 'work': { 'reportsToIdInCompany' : 64, 'employeeIdInCompany' : 140, - 'reportsTo': {{ + 'reportsTo': { 'email': 'somebody@nomail.com' - }} - }}, + } + }, 'Link': 'https://www.adoptapet.com/public/apis/pet_list.html', 'Category': 'Animals' - }}, - {{ + }, + { 'API': 'Axolotl', 'Description': 'Collection of axolotl pictures and facts', 'Auth': '', 'HTTPS': true, 'Cors': 'no', - 'work': {{ + 'work': { 'reportsToIdInCompany' : 50, 'employeeIdInCompany' : 160, - 'reportsTo': {{ + 'reportsTo': { 'email': 'somebodyelse@nomail.com' - }} - }}, + } + }, 'Link': 'https://theaxolotlapi.netlify.app/', 'Category': 'Animals' - }} + } ] - }} + } "; - } - - [Test] - public void TestMapperReturnsCollectionWithMappingConfigurationCorrectWithNestedProperties() + } + + [Test] + public void TestMapperReturnsCollectionWithMappingConfigurationCorrectWithNestedProperties() + { + var destinationCollection = new List(); + var nestedResult = SampleCollectionWithNestedObjectsAsProperties(); + var result = nestedResult.MapCollection(destinationCollection, options => { - var destinationCollection = new List(); - var nestedResult = SampleCollectionWithNestedObjectsAsProperties(); - var result = nestedResult.MapCollection(destinationCollection, options => + options.RootKey = "entries"; + options.Mappings = new Dictionary { - options.RootKey = "entries"; - options.Mappings = new Dictionary - { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "CategoryVariation", "Category" }, - { "ReportsToIdInCompanyVariation", "work.reportsToIdInCompany" }, - { "ReportsToEmailVariation", "work.reportsTo.email"} - }; - }); - - var firstItem = result.FirstOrDefault(); - - Assert.That(result, Is.EqualTo(destinationCollection)); - Assert.That(firstItem.DescriptionVariation, Is.Not.Null); - } + { "DescriptionVariation", "Description" }, + { "AuthVariation", "Auth" }, + { "CategoryVariation", "Category" }, + { "ReportsToIdInCompanyVariation", "work.reportsToIdInCompany" }, + { "ReportsToEmailVariation", "work.reportsTo.email" } + }; + }); + + var firstItem = result.FirstOrDefault(); + + Assert.That(result, Is.EqualTo(destinationCollection)); + Assert.That(firstItem.DescriptionVariation, Is.Not.Null); } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj index b38690a..be96a72 100644 --- a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj @@ -1,34 +1,35 @@  - - netstandard2.0 - Custom Auto Adapter Mapper - Aghogho Bernard - Custom Mapper; JSON Response Mapper; Endpoint Adapter; Newtonsoft; - In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be implemented is also challenging, as development work is needed each time. + + netstandard2.0 + Custom Auto Adapter Mapper + Aghogho Bernard + Custom Mapper; JSON Response Mapper; Endpoint Adapter; Newtonsoft; + In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be implemented is also challenging, as development work is needed each time. -Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. Hence, custom mapping needs to be established. + Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. Hence, custom mapping needs to be established. -This library solves the problems of mapping a JSON string to a known type. - https://github.com/teghoz/CustomAutoAdapterMapper - README.md - license.txt - 1.0.3 - + This library solves the problems of mapping a JSON string to a known type. + + https://github.com/teghoz/CustomAutoAdapterMapper + README.md + license.txt + 1.0.3 + - - - True - \ - - - True - \ - - + + + True + \ + + + True + \ + + - - - + + + diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.sln.DotSettings.user b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.sln.DotSettings.user new file mode 100644 index 0000000..2838ba8 --- /dev/null +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.sln.DotSettings.user @@ -0,0 +1,4 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="TestMapperReturnsCollectionWithMappingConfigurationCorrectWithNestedProperties" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Solution /> +</SessionState> \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Exceptions/ItemKeyOptionNullException.cs b/CustomAutoAdapterMapper/Exceptions/ItemKeyOptionNullException.cs index 54a9cb5..2c9cc04 100644 --- a/CustomAutoAdapterMapper/Exceptions/ItemKeyOptionNullException.cs +++ b/CustomAutoAdapterMapper/Exceptions/ItemKeyOptionNullException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Text; namespace CustomAutoAdapterMapper.Exceptions { @@ -19,4 +16,4 @@ public ItemKeyOptionNullException(string message, Exception innerException) : ba { } } -} +} \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Exceptions/JsonContentException.cs b/CustomAutoAdapterMapper/Exceptions/JsonContentException.cs index f2c8d30..147f00c 100644 --- a/CustomAutoAdapterMapper/Exceptions/JsonContentException.cs +++ b/CustomAutoAdapterMapper/Exceptions/JsonContentException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Text; namespace CustomAutoAdapterMapper.Exceptions { @@ -19,4 +16,4 @@ public JsonContentException(string message, Exception innerException) : base(mes { } } -} +} \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Exceptions/RootKeyOptionNullException.cs b/CustomAutoAdapterMapper/Exceptions/RootKeyOptionNullException.cs index e5b85c3..487dda4 100644 --- a/CustomAutoAdapterMapper/Exceptions/RootKeyOptionNullException.cs +++ b/CustomAutoAdapterMapper/Exceptions/RootKeyOptionNullException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Text; namespace CustomAutoAdapterMapper.Exceptions { @@ -19,4 +16,4 @@ public RootKeyOptionNullException(string message, Exception innerException) : ba { } } -} +} \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Exceptions/RootKeyPropertyNullException.cs b/CustomAutoAdapterMapper/Exceptions/RootKeyPropertyNullException.cs index 257e7d5..3b00f5f 100644 --- a/CustomAutoAdapterMapper/Exceptions/RootKeyPropertyNullException.cs +++ b/CustomAutoAdapterMapper/Exceptions/RootKeyPropertyNullException.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Text; namespace CustomAutoAdapterMapper.Exceptions { @@ -19,4 +16,4 @@ public RootKeyPropertyNullException(string message, Exception innerException) : { } } -} +} \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 9f5066e..c0a05d9 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -1,10 +1,10 @@ -using CustomAutoAdapterMapper.Exceptions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using CustomAutoAdapterMapper.Exceptions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace CustomAutoAdapterMapper { @@ -19,27 +19,22 @@ public static List MapCollection(this string jsonResponse, List destina var rootProperty = Validate(jsonResponse, jsonObject, mapperOptions); var entries = rootProperty.ToList(); - if(MapperShouldIterateThroughEntireIncomingCollection(destination, mapperOptions)) - { + if (MapperShouldIterateThroughEntireIncomingCollection(destination, mapperOptions)) foreach (var entry in entries) { - T collectionItem = Activator.CreateInstance(); + var collectionItem = Activator.CreateInstance(); SeedCollectionItem(entry, collectionItem); SeedMappedPropertiesOfItem(entry, collectionItem, mapperOptions); destination.Add(collectionItem); } - } else - { foreach (var entry in destination) - { SeedKnownCollectionOfItem(entries, entry, mapperOptions); - } - } return destination; } + private static bool JsonStringIsValid(string jsonString) { try @@ -52,15 +47,19 @@ private static bool JsonStringIsValid(string jsonString) return false; } } + private static bool MapperShouldIterateThroughEntireIncomingCollection(List destination, Option options) { var itemKeyIdentifierIsEmpty = destination != null && !string.IsNullOrEmpty(options.ItemKey) && - destination.All(x => string.IsNullOrEmpty(x.GetType()?.GetProperty(options.ItemKey)?.GetValue(x)?.ToString() ?? string.Empty)); + destination.All(x => + string.IsNullOrEmpty(x.GetType()?.GetProperty(options.ItemKey) + ?.GetValue(x)?.ToString() ?? string.Empty)); var result = destination == null || destination.Count == 0 || itemKeyIdentifierIsEmpty; return result; } + private static void SeedCollectionItem(JToken entry, T collectionItem) { var collectionItemProperties = collectionItem @@ -74,6 +73,7 @@ private static void SeedCollectionItem(JToken entry, T collectionItem) SetPropertyValue(collectionItem, property.Name, mappedValue); } } + private static void SeedMappedPropertiesOfItem(JToken entry, T collectionItem, Option mapperOptions) { var matchedProperties = collectionItem @@ -88,39 +88,37 @@ private static void SeedMappedPropertiesOfItem(JToken entry, T collectionItem if (entry[incomingProperty] != null) { - var mappedPropertyValue = entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; + var mappedPropertyValue = + entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; if (mappedPropertyValue != null) - { SetPropertyValue(collectionItem, property.Name, mappedPropertyValue); - } } else { - var mappedPropertyValue = entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; + var mappedPropertyValue = + entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; if (mappedPropertyValue != null) - { SetPropertyValue(collectionItem, property.Name, mappedPropertyValue); - } } } } + private static void SeedKnownCollectionOfItem(List entries, T entry, Option mapperOptions) { if (string.IsNullOrEmpty(mapperOptions.ItemKey)) - { throw new ItemKeyOptionNullException("Item Key Option Not Set!!!!!"); - } var keyItemExist = entry.GetType().GetProperty(mapperOptions.ItemKey); if (keyItemExist == null) return; - var propertyKeyItemValue = entry.GetType()?.GetProperty(mapperOptions.ItemKey)?.GetValue(entry)?.ToString() ?? null; + var propertyKeyItemValue = + entry.GetType()?.GetProperty(mapperOptions.ItemKey)?.GetValue(entry)?.ToString() ?? null; if (propertyKeyItemValue == null) return; var incomingRecord = entries .Where(e => e.Values().Any(ee => ee.ToString() == propertyKeyItemValue)) .FirstOrDefault(); - + var matchedProperties = entry .GetType() .GetProperties() @@ -135,53 +133,35 @@ private static void SeedKnownCollectionOfItem(List entries, T entry, { var mappedValue = incomingRecord[incomingProperty].ToString(); - if (!string.IsNullOrEmpty(mappedValue)) - { - SetPropertyValue(entry, property.Name, mappedValue); - } + if (!string.IsNullOrEmpty(mappedValue)) SetPropertyValue(entry, property.Name, mappedValue); } } } + private static JToken Validate(string jsonResponse, JObject jsonObject, Option option) { - if (JsonStringIsValid(jsonResponse) == false) - { - throw new JsonContentException("Json Content Supplied Is Invalid"); - } + if (!JsonStringIsValid(jsonResponse)) throw new JsonContentException("Json Content Supplied Is Invalid"); if (string.IsNullOrEmpty(option.RootKey)) - { throw new RootKeyOptionNullException("Root Key Is Required to map"); - } - try - { - jsonObject = JObject.Parse(jsonResponse); - } - catch (Exception) - { - throw; - } + jsonObject = JObject.Parse(jsonResponse); var rootProperty = jsonObject[option.RootKey]; if (rootProperty == null) - { throw new RootKeyPropertyNullException("Root Property Does Not Exist In Object!!!!!"); - } return rootProperty; } + private static void SetPropertyValue(T destinationObject, string propertyName, object value) { var prop = destinationObject .GetType() .GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); - - if (prop != null && prop.CanWrite) - { - prop.SetValue(destinationObject, value, null); - } + + if (prop != null && prop.CanWrite) prop.SetValue(destinationObject, value, null); } } -} +} \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Option.cs b/CustomAutoAdapterMapper/Option.cs index 05c6d46..c47cb22 100644 --- a/CustomAutoAdapterMapper/Option.cs +++ b/CustomAutoAdapterMapper/Option.cs @@ -8,21 +8,17 @@ public class Option public string RootKey { get; set; } public string ItemKey { get; set; } public Dictionary Mappings { get; set; } + public List MappingKeys { get { - if (Mappings == null) - { - return new List(); - } - else - { - return Mappings - .Select(k => k.Key.ToString()) - .ToList(); - } + if (Mappings == null) return new List(); + + return Mappings + .Select(k => k.Key.ToString()) + .ToList(); } } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 02d05db..f0fdd06 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # CustomAutoAdapterMapper -In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be implemented is also challenging, as development work is needed each time. -Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. Hence, custom mapping needs to be established. +In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown +types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be +implemented is also challenging, as development work is needed each time. + +Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. +Hence, custom mapping needs to be established. This library solves the problems of mapping a JSON string to a known type. @@ -64,7 +68,8 @@ result.MapCollection(destinationCollection.Entries, options => }); ``` -The dictionary In the example above maps the value of `Description` in `TestObject` to `DescriptionVariation` in `TestObjectWithVariation` +The dictionary In the example above maps the value of `Description` in `TestObject` to `DescriptionVariation` in +`TestObjectWithVariation` ```C# public class TestObjectWithVariation @@ -95,9 +100,10 @@ public class TestObject ``` # Options -Value | Description -------------- | ------------- -RootKey | Specifies what Property holds the collection that needs to be mapped -Mappings | Specified the dictionary that holds the custom property/field mappings needed -ItemKey | Used to Identify the Unique Identifier of an item in the collection + + Value | Description +----------|------------------------------------------------------------------------------- + RootKey | Specifies what Property holds the collection that needs to be mapped + Mappings | Specified the dictionary that holds the custom property/field mappings needed + ItemKey | Used to Identify the Unique Identifier of an item in the collection From 219076660696678180d44422ba02d12ef90c78e8 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 24 Oct 2025 16:32:24 -0230 Subject: [PATCH 02/10] use selectToken --- CustomAutoAdapterMapper/Mapper.cs | 18 +- README.md | 477 ++++++++++++++++++++++++++---- 2 files changed, 423 insertions(+), 72 deletions(-) diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index c0a05d9..01e3832 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -86,21 +86,11 @@ private static void SeedMappedPropertiesOfItem(JToken entry, T collectionItem { var incomingProperty = mapperOptions.Mappings[property.Name]; - if (entry[incomingProperty] != null) - { - var mappedPropertyValue = - entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; + var mappedPropertyValue = + entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; - if (mappedPropertyValue != null) - SetPropertyValue(collectionItem, property.Name, mappedPropertyValue); - } - else - { - var mappedPropertyValue = - entry.SelectToken(incomingProperty)?.ToObject(property.PropertyType) ?? null; - if (mappedPropertyValue != null) - SetPropertyValue(collectionItem, property.Name, mappedPropertyValue); - } + if (mappedPropertyValue != null) + SetPropertyValue(collectionItem, property.Name, mappedPropertyValue); } } diff --git a/README.md b/README.md index f0fdd06..64f195a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,81 @@ # CustomAutoAdapterMapper -In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown -types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be -implemented is also challenging, as development work is needed each time. +[![NuGet](https://img.shields.io/nuget/v/CustomAutoAdapterMapper.svg)](https://www.nuget.org/packages/CustomAutoAdapterMapper/) +[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-blue.svg)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. -Hence, custom mapping needs to be established. +A lightweight, flexible JSON-to-object mapper for C# that handles third-party API responses with mismatched property names and nested structures without requiring contract definitions. -This library solves the problems of mapping a JSON string to a known type. +## 🎯 Problem Statement -# Usage +In organizations that integrate with multiple external systems: -Example endpoint: https://api.publicapis.org/entries +- **Strongly-typed languages** like C# make it difficult to map unknown or dynamic JSON structures at runtime +- **Creating contracts** for every third-party API is time-consuming and requires development work for each new integration +- **Property mismatches** between external APIs and internal models require custom mapping logic +- **Nested properties** in JSON need to be flattened or mapped to different structures -```JSON +**CustomAutoAdapterMapper** solves these challenges by providing a flexible, configuration-driven approach to mapping JSON strings to strongly-typed C# objects. + +--- + +## 📦 Installation + +```bash +dotnet add package CustomAutoAdapterMapper +``` + +Or via NuGet Package Manager: + +```bash +Install-Package CustomAutoAdapterMapper +``` + +--- + +## 🚀 Quick Start + +### Basic Usage: Direct Property Mapping + +When JSON property names match your C# class properties: + +```csharp +using CustomAutoAdapterMapper; + +var jsonResponse = await httpClient.GetStringAsync("https://api.example.com/data"); +var destinationCollection = new List(); + +var result = jsonResponse.MapCollection(destinationCollection, options => +{ + options.RootKey = "entries"; // JSON property containing the array +}); +``` + +### Custom Property Mapping + +When JSON property names differ from your C# class properties: + +```csharp +var result = jsonResponse.MapCollection(destinationCollection, options => +{ + options.RootKey = "entries"; + options.Mappings = new Dictionary + { + { "MyProperty", "TheirProperty" }, // Map TheirProperty -> MyProperty + { "Description", "desc" }, // Map desc -> Description + { "AuthType", "authentication_type" } // Map authentication_type -> AuthType + }; +}); +``` + +--- + +## 📖 Comprehensive Examples + +### Example 1: Simple Mapping with Variations + +**JSON Response** from `https://api.publicapis.org/entries`: + +```json { "count": 1427, "entries": [ @@ -24,13 +86,7 @@ Example endpoint: https://api.publicapis.org/entries "HTTPS": true, "Cors": "yes", "Link": "https://www.adoptapet.com/public/apis/pet_list.html", - "Category": "Animals", - "Parent": { - "SomeProperties": "ABC", - "SubParent": { - "SubParentProperty": "123" - } - } + "Category": "Animals" }, { "API": "Axolotl", @@ -39,71 +95,376 @@ Example endpoint: https://api.publicapis.org/entries "HTTPS": true, "Cors": "no", "Link": "https://theaxolotlapi.netlify.app/", - "Category": "Animals", - "Parent": { - "SomeProperties": "DEF", - "SubParent": { - "SubParentProperty": "456" - } - } + "Category": "Animals" } ] } ``` -```C# -var result = JSON_STRING_FROM_ENDPOINT; -var destinationCollection = new List(); -result.MapCollection(destinationCollection.Entries, options => +**Your C# Model** (with different property names): + +```csharp +public class ApiEntry +{ + public string API { get; set; } + public string DescriptionText { get; set; } // Different name + public string AuthType { get; set; } // Different name + public bool HTTPS { get; set; } + public string Cors { get; set; } + public string Link { get; set; } + public string CategoryName { get; set; } // Different name +} +``` + +**Mapping Code**: + +```csharp +var destinationCollection = new List(); +var result = jsonResponse.MapCollection(destinationCollection, options => { options.RootKey = "entries"; - options.ItemKey = "API"; options.Mappings = new Dictionary { - { "DescriptionVariation", "Description" }, - { "AuthVariation", "Auth" }, - { "PropertyOne", "Parent.SomeProperties" }, - { "PropertyTwo", "Parent.SubParent.SubParentProperty" }, + { "DescriptionText", "Description" }, + { "AuthType", "Auth" }, + { "CategoryName", "Category" } }; }); ``` -The dictionary In the example above maps the value of `Description` in `TestObject` to `DescriptionVariation` in -`TestObjectWithVariation` +### Example 2: Nested Property Mapping (Dot Notation) + +Map deeply nested JSON properties to flat C# properties using **dot notation**. + +**JSON Response**: -```C# -public class TestObjectWithVariation +```json { - public string API { get; set; } - public string DescriptionVariation { get; set; } - public string AuthVariation { get; set; } - public string HTTPS { get; set; } - public string Cors { get; set; } - public string Link { get; set; } - public string CategoryVariation { get; set; } - public string PropertyOne { get; set; } - public string PropertyTwo { get; set; } + "entries": [ + { + "API": "AdoptAPet", + "Description": "Resource to help get pets adopted", + "work": { + "reportsToIdInCompany": 64, + "employeeIdInCompany": 140, + "reportsTo": { + "email": "manager@company.com", + "name": "John Doe" + } + } + } + ] } ``` -```C# -public class TestObject +**Your C# Model** (flattened structure): + +```csharp +public class Employee { public string API { get; set; } public string Description { get; set; } - public string Auth { get; set; } - public string HTTPS { get; set; } - public string Cors { get; set; } - public string Link { get; set; } - public string Category { get; set; } + public int ManagerId { get; set; } + public int EmployeeId { get; set; } + public string ManagerEmail { get; set; } + public string ManagerName { get; set; } +} +``` + +**Mapping Code**: + +```csharp +var employees = new List(); +var result = jsonResponse.MapCollection(employees, options => +{ + options.RootKey = "entries"; + options.Mappings = new Dictionary + { + { "ManagerId", "work.reportsToIdInCompany" }, // Nested property + { "EmployeeId", "work.employeeIdInCompany" }, // Nested property + { "ManagerEmail", "work.reportsTo.email" }, // Deeply nested + { "ManagerName", "work.reportsTo.name" } // Deeply nested + }; +}); +``` + +### Example 3: Updating Existing Collections + +Use `ItemKey` to update an existing collection instead of creating a new one. + +**Scenario**: You have a pre-populated list and want to update specific items based on a unique identifier. + +```csharp +// Pre-populated collection +var existingApis = new List +{ + new ApiEntry { API = "AdoptAPet", DescriptionText = "Old description" }, + new ApiEntry { API = "Axolotl", DescriptionText = "Old description" } +}; + +// Update the collection with fresh data from the API +var result = jsonResponse.MapCollection(existingApis, options => +{ + options.RootKey = "entries"; + options.ItemKey = "API"; // Match items by the "API" property + options.Mappings = new Dictionary + { + { "DescriptionText", "Description" }, + { "AuthType", "Auth" } + }; +}); + +// Only mapped properties are updated; other properties remain unchanged +``` + +--- + +## ⚙️ Configuration Options + +### `Option` Class Properties + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| **`RootKey`** | `string` | ✅ Yes | The JSON property name that contains the array/collection to map. | +| **`Mappings`** | `Dictionary` | ⚠️ Optional | Custom property mappings. **Key** = your C# property name, **Value** = JSON property path (supports dot notation for nested properties). | +| **`ItemKey`** | `string` | ⚠️ Conditional | Unique identifier property name. **Required** when updating an existing non-empty collection. Used to match items between JSON and your collection. | + +### Configuration Details + +#### `RootKey` +- Identifies which JSON property contains the array of items to map +- Must be a valid property in the root JSON object +- **Throws `RootKeyOptionNullException`** if not provided +- **Throws `RootKeyPropertyNullException`** if the property doesn't exist in the JSON + +#### `Mappings` +- Optional dictionary for custom property mappings +- **Key**: Your C# class property name +- **Value**: JSON property path (supports nested properties with dot notation) +- If not provided, the mapper attempts direct property name matching + +**Examples**: +```csharp +options.Mappings = new Dictionary +{ + { "MyProperty", "their_property" }, // Simple mapping + { "Email", "user.contact.email" }, // Nested property + { "ManagerId", "employee.reports_to.id" } // Deeply nested +}; +``` + +#### `ItemKey` +- Specifies a unique identifier property for matching items +- **Required when**: + - Updating an existing collection (non-empty `List`) + - You want to preserve existing items and only update mapped properties +- **Not required when**: + - Creating a new collection from scratch (empty or null list) +- **Throws `ItemKeyOptionNullException`** if required but not provided + +--- + +## 🔍 How It Works + +### Mapping Behavior + +The mapper operates in two modes: + +#### 1. **Create Mode** (Empty/Null Collection) +When you pass an empty or null collection: +- Creates new instances of your type `T` +- Maps all matching properties automatically +- Applies custom mappings from `options.Mappings` +- Adds items to your collection + +```csharp +var newCollection = new List(); // Empty collection +jsonResponse.MapCollection(newCollection, options => { + options.RootKey = "data"; + // ItemKey not required +}); +``` + +#### 2. **Update Mode** (Existing Collection) +When you pass a non-empty collection: +- Matches items using `ItemKey` +- Only updates properties defined in `options.Mappings` +- Preserves all other properties in existing items +- Does not add new items + +```csharp +var existingCollection = GetExistingData(); // Non-empty collection +jsonResponse.MapCollection(existingCollection, options => { + options.RootKey = "data"; + options.ItemKey = "Id"; // Required! + options.Mappings = new Dictionary { /* ... */ }; +}); +``` + +### Type Conversion + +- The mapper uses **`Newtonsoft.Json`** for type conversion +- Automatically converts JSON types to C# property types +- Supports: + - Primitives (`string`, `int`, `bool`, `decimal`, etc.) + - Nullable types (`int?`, `DateTime?`, etc.) + - Complex types (nested objects) + - Collections and arrays + +--- + +## ⚠️ Exception Handling + +The library throws custom exceptions for common configuration errors: + +| Exception | When Thrown | Solution | +|-----------|-------------|----------| +| **`JsonContentException`** | The provided string is not valid JSON | Ensure the input string is valid JSON | +| **`RootKeyOptionNullException`** | `RootKey` is not provided in options | Set `options.RootKey` to the JSON array property name | +| **`RootKeyPropertyNullException`** | `RootKey` doesn't exist in the JSON object | Verify the JSON structure and `RootKey` value | +| **`ItemKeyOptionNullException`** | `ItemKey` is required but not provided (when updating existing collections) | Set `options.ItemKey` to a unique identifier property | +| **`JsonReaderException`** | JSON cannot be parsed as an object (e.g., it's a raw array) | Ensure JSON is an object with a root property containing the array | + +### Error Handling Example + +```csharp +try +{ + var result = jsonResponse.MapCollection(collection, options => + { + options.RootKey = "entries"; + }); +} +catch (JsonContentException ex) +{ + // Invalid JSON string + Console.WriteLine($"Invalid JSON: {ex.Message}"); +} +catch (RootKeyPropertyNullException ex) +{ + // RootKey doesn't exist in JSON + Console.WriteLine($"Property not found: {ex.Message}"); +} +catch (ItemKeyOptionNullException ex) +{ + // ItemKey required but not provided + Console.WriteLine($"Missing ItemKey: {ex.Message}"); } ``` -# Options +--- + +## 🎓 Advanced Usage + +### Complex Nested Structures + +You can map multiple levels of nesting: + +```csharp +options.Mappings = new Dictionary +{ + { "Street", "address.street" }, + { "City", "address.city" }, + { "ZipCode", "address.location.zipCode" }, + { "Country", "address.location.country.name" }, + { "CountryCode", "address.location.country.code" } +}; +``` + +### Combining Direct and Custom Mappings + +Properties not in `Mappings` are mapped directly by name: + +```csharp +public class Product +{ + public string Id { get; set; } // Mapped directly from JSON "Id" + public string Name { get; set; } // Mapped directly from JSON "Name" + public decimal Cost { get; set; } // Custom mapping required +} + +options.Mappings = new Dictionary +{ + { "Cost", "pricing.unitPrice" } // Only Cost needs custom mapping +}; +// Id and Name are automatically mapped if they exist in the JSON +``` + +### Type Safety with Nullability + +The mapper handles null values gracefully: + +```csharp +public class SafeModel +{ + public string Required { get; set; } // Will be null if not in JSON + public int? OptionalNumber { get; set; } // Nullable type + public DateTime? OptionalDate { get; set; } +} +``` + +--- + +## 🧪 Testing + +The library includes comprehensive unit tests covering: + +- ✅ Basic property mapping +- ✅ Custom property mappings +- ✅ Nested property mapping with dot notation +- ✅ Collection creation (empty destination) +- ✅ Collection updates (existing destination with ItemKey) +- ✅ Exception scenarios +- ✅ Type conversions + +Run tests: + +```bash +dotnet test +``` + +--- + +## 🛠️ Technical Details + +- **Target Framework**: .NET Standard 2.0 +- **Dependencies**: Newtonsoft.Json (>= 13.0.3) +- **Namespace**: `CustomAutoAdapterMapper` +- **Primary Method**: `MapCollection` (extension method on `string`) + +--- + +## 📝 Best Practices + +1. **Always set `RootKey`** - It's required and identifies your data array +2. **Use `ItemKey` for updates** - When updating existing collections, always specify a unique identifier +3. **Leverage dot notation** - For nested properties, use `"parent.child.property"` syntax +4. **Handle exceptions** - Wrap mapping calls in try-catch for production code +5. **Validate JSON first** - Ensure external API responses are valid before mapping +6. **Use nullable types** - For optional properties, use nullable types (`int?`, `DateTime?`, etc.) + +--- + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +--- + +## 📄 License + +See [license.txt](license.txt) for details. + +--- + +## 🔗 Links + +- **NuGet Package**: https://www.nuget.org/packages/CustomAutoAdapterMapper/ +- **GitHub Repository**: https://github.com/teghoz/CustomAutoAdapterMapper + +--- + +## 📧 Support - Value | Description -----------|------------------------------------------------------------------------------- - RootKey | Specifies what Property holds the collection that needs to be mapped - Mappings | Specified the dictionary that holds the custom property/field mappings needed - ItemKey | Used to Identify the Unique Identifier of an item in the collection +For issues, questions, or feature requests, please open an issue on GitHub From 18bddbf90090a21a8fa950c9179249ac7c2804ee Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 24 Oct 2025 17:25:05 -0230 Subject: [PATCH 03/10] Feature/fix dot notation for custom mapping (#10) * use selectToken * updates --------- Co-authored-by: Aghogho Bernard --- .../CustomAutoAdapterMapper.csproj | 13 +++++-------- CustomAutoAdapterMapper/Mapper.cs | 10 +++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj index be96a72..9b5cddb 100644 --- a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj @@ -4,17 +4,14 @@ netstandard2.0 Custom Auto Adapter Mapper Aghogho Bernard - Custom Mapper; JSON Response Mapper; Endpoint Adapter; Newtonsoft; - In Organizations that synchronize information from different systems supplied by specified endpoints, mapping unknown types in real-time is a pain, considering C# is "strongly typed." Creating contracts for every third-party system to be implemented is also challenging, as development work is needed each time. - - Additionally, most properties or fields supplied might not match the expected properties or fields of the known type. Hence, custom mapping needs to be established. - - This library solves the problems of mapping a JSON string to a known type. - + Custom Mapper; JSON Response Mapper; Endpoint Adapter; Newtonsoft; JSON Mapper; API Mapper; Nested Properties; + A lightweight, flexible JSON-to-object mapper for C# that handles third-party API responses with mismatched property names and nested structures. Supports dot notation for mapping nested JSON properties (e.g., "parent.child.property") to flat C# objects without requiring contract definitions. Perfect for integrating with multiple external APIs. https://github.com/teghoz/CustomAutoAdapterMapper README.md license.txt - 1.0.3 + 1.0.5 + v1.0.5: Fixed nested property mapping with dot notation in both create and update modes. Added null check for incomingRecord. Improved documentation with comprehensive examples. + https://github.com/teghoz/CustomAutoAdapterMapper diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 01e3832..94cac39 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -109,6 +109,8 @@ private static void SeedKnownCollectionOfItem(List entries, T entry, .Where(e => e.Values().Any(ee => ee.ToString() == propertyKeyItemValue)) .FirstOrDefault(); + if (incomingRecord == null) return; + var matchedProperties = entry .GetType() .GetProperties() @@ -119,12 +121,10 @@ private static void SeedKnownCollectionOfItem(List entries, T entry, { var incomingProperty = mapperOptions.Mappings[property.Name]; - if (incomingRecord[incomingProperty] != null) - { - var mappedValue = incomingRecord[incomingProperty].ToString(); + var mappedValue = incomingRecord.SelectToken(incomingProperty)?.ToString(); - if (!string.IsNullOrEmpty(mappedValue)) SetPropertyValue(entry, property.Name, mappedValue); - } + if (!string.IsNullOrEmpty(mappedValue)) + SetPropertyValue(entry, property.Name, mappedValue); } } From 01ec26a4382877dcac012c166d92deee4b639cc6 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 9 Jan 2026 17:23:05 -0330 Subject: [PATCH 04/10] Check if itemkey is mapped and check is the objects has the mapped value when the supplied itemkey is not present (#11) Co-authored-by: Aghogho Bernard --- CustomAutoAdapterMapper/Mapper.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 94cac39..1059fb6 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -56,6 +56,14 @@ private static bool MapperShouldIterateThroughEntireIncomingCollection(List + string.IsNullOrEmpty(x.GetType()?.GetProperty(mappedPropertyName) + ?.GetValue(x)?.ToString() ?? string.Empty)); + } + var result = destination == null || destination.Count == 0 || itemKeyIdentifierIsEmpty; return result; } From bd3d71c7ffe2787d61cf4395bc67396da8c6f035 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 9 Jan 2026 17:58:54 -0330 Subject: [PATCH 05/10] iteration decision making (#13) * Check if itemkey is mapped and check is the objects has the mapped value when the supplied itemkey is not present * updated package version * bug fix for iteration decision --------- Co-authored-by: Aghogho Bernard --- .../CustomAutoAdapterMapper.csproj | 4 +- CustomAutoAdapterMapper/Mapper.cs | 3 +- README.md | 55 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj index 9b5cddb..e36158a 100644 --- a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj @@ -9,8 +9,8 @@ https://github.com/teghoz/CustomAutoAdapterMapper README.md license.txt - 1.0.5 - v1.0.5: Fixed nested property mapping with dot notation in both create and update modes. Added null check for incomingRecord. Improved documentation with comprehensive examples. + 1.0.6 + v1.0.6: Bug fixes and improvements. https://github.com/teghoz/CustomAutoAdapterMapper diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 1059fb6..429ba6b 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -56,12 +56,13 @@ private static bool MapperShouldIterateThroughEntireIncomingCollection(List string.IsNullOrEmpty(x.GetType()?.GetProperty(mappedPropertyName) ?.GetValue(x)?.ToString() ?? string.Empty)); + options.ItemKey = mappedPropertyName; } var result = destination == null || destination.Count == 0 || itemKeyIdentifierIsEmpty; diff --git a/README.md b/README.md index 64f195a..6519162 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,21 @@ [![NuGet](https://img.shields.io/nuget/v/CustomAutoAdapterMapper.svg)](https://www.nuget.org/packages/CustomAutoAdapterMapper/) [![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-blue.svg)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) -A lightweight, flexible JSON-to-object mapper for C# that handles third-party API responses with mismatched property names and nested structures without requiring contract definitions. +A lightweight, flexible JSON-to-object mapper for C# that handles third-party API responses with mismatched property +names and nested structures without requiring contract definitions. ## 🎯 Problem Statement In organizations that integrate with multiple external systems: - **Strongly-typed languages** like C# make it difficult to map unknown or dynamic JSON structures at runtime -- **Creating contracts** for every third-party API is time-consuming and requires development work for each new integration +- **Creating contracts** for every third-party API is time-consuming and requires development work for each new + integration - **Property mismatches** between external APIs and internal models require custom mapping logic - **Nested properties** in JSON need to be flattened or mapped to different structures -**CustomAutoAdapterMapper** solves these challenges by providing a flexible, configuration-driven approach to mapping JSON strings to strongly-typed C# objects. +**CustomAutoAdapterMapper** solves these challenges by providing a flexible, configuration-driven approach to mapping +JSON strings to strongly-typed C# objects. --- @@ -223,27 +226,30 @@ var result = jsonResponse.MapCollection(existingApis, options => ### `Option` Class Properties -| Property | Type | Required | Description | -|----------|------|----------|-------------| -| **`RootKey`** | `string` | ✅ Yes | The JSON property name that contains the array/collection to map. | -| **`Mappings`** | `Dictionary` | ⚠️ Optional | Custom property mappings. **Key** = your C# property name, **Value** = JSON property path (supports dot notation for nested properties). | -| **`ItemKey`** | `string` | ⚠️ Conditional | Unique identifier property name. **Required** when updating an existing non-empty collection. Used to match items between JSON and your collection. | +| Property | Type | Required | Description | +|----------------|------------------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| **`RootKey`** | `string` | ✅ Yes | The JSON property name that contains the array/collection to map. | +| **`Mappings`** | `Dictionary` | ⚠️ Optional | Custom property mappings. **Key** = your C# property name, **Value** = JSON property path (supports dot notation for nested properties). | +| **`ItemKey`** | `string` | ⚠️ Conditional | Unique identifier property name. **Required** when updating an existing non-empty collection. Used to match items between JSON and your collection. | ### Configuration Details #### `RootKey` + - Identifies which JSON property contains the array of items to map - Must be a valid property in the root JSON object - **Throws `RootKeyOptionNullException`** if not provided - **Throws `RootKeyPropertyNullException`** if the property doesn't exist in the JSON #### `Mappings` + - Optional dictionary for custom property mappings - **Key**: Your C# class property name - **Value**: JSON property path (supports nested properties with dot notation) - If not provided, the mapper attempts direct property name matching **Examples**: + ```csharp options.Mappings = new Dictionary { @@ -254,12 +260,13 @@ options.Mappings = new Dictionary ``` #### `ItemKey` + - Specifies a unique identifier property for matching items - **Required when**: - - Updating an existing collection (non-empty `List`) - - You want to preserve existing items and only update mapped properties + - Updating an existing collection (non-empty `List`) + - You want to preserve existing items and only update mapped properties - **Not required when**: - - Creating a new collection from scratch (empty or null list) + - Creating a new collection from scratch (empty or null list) - **Throws `ItemKeyOptionNullException`** if required but not provided --- @@ -271,7 +278,9 @@ options.Mappings = new Dictionary The mapper operates in two modes: #### 1. **Create Mode** (Empty/Null Collection) + When you pass an empty or null collection: + - Creates new instances of your type `T` - Maps all matching properties automatically - Applies custom mappings from `options.Mappings` @@ -286,7 +295,9 @@ jsonResponse.MapCollection(newCollection, options => { ``` #### 2. **Update Mode** (Existing Collection) + When you pass a non-empty collection: + - Matches items using `ItemKey` - Only updates properties defined in `options.Mappings` - Preserves all other properties in existing items @@ -306,10 +317,10 @@ jsonResponse.MapCollection(existingCollection, options => { - The mapper uses **`Newtonsoft.Json`** for type conversion - Automatically converts JSON types to C# property types - Supports: - - Primitives (`string`, `int`, `bool`, `decimal`, etc.) - - Nullable types (`int?`, `DateTime?`, etc.) - - Complex types (nested objects) - - Collections and arrays + - Primitives (`string`, `int`, `bool`, `decimal`, etc.) + - Nullable types (`int?`, `DateTime?`, etc.) + - Complex types (nested objects) + - Collections and arrays --- @@ -317,13 +328,13 @@ jsonResponse.MapCollection(existingCollection, options => { The library throws custom exceptions for common configuration errors: -| Exception | When Thrown | Solution | -|-----------|-------------|----------| -| **`JsonContentException`** | The provided string is not valid JSON | Ensure the input string is valid JSON | -| **`RootKeyOptionNullException`** | `RootKey` is not provided in options | Set `options.RootKey` to the JSON array property name | -| **`RootKeyPropertyNullException`** | `RootKey` doesn't exist in the JSON object | Verify the JSON structure and `RootKey` value | -| **`ItemKeyOptionNullException`** | `ItemKey` is required but not provided (when updating existing collections) | Set `options.ItemKey` to a unique identifier property | -| **`JsonReaderException`** | JSON cannot be parsed as an object (e.g., it's a raw array) | Ensure JSON is an object with a root property containing the array | +| Exception | When Thrown | Solution | +|------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------------------------------| +| **`JsonContentException`** | The provided string is not valid JSON | Ensure the input string is valid JSON | +| **`RootKeyOptionNullException`** | `RootKey` is not provided in options | Set `options.RootKey` to the JSON array property name | +| **`RootKeyPropertyNullException`** | `RootKey` doesn't exist in the JSON object | Verify the JSON structure and `RootKey` value | +| **`ItemKeyOptionNullException`** | `ItemKey` is required but not provided (when updating existing collections) | Set `options.ItemKey` to a unique identifier property | +| **`JsonReaderException`** | JSON cannot be parsed as an object (e.g., it's a raw array) | Ensure JSON is an object with a root property containing the array | ### Error Handling Example From 3bfdb989589c899fb47a8ca53bd3a84ad1ad67fb Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Fri, 9 Jan 2026 18:07:33 -0330 Subject: [PATCH 06/10] update version --- CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj index e36158a..1d141a4 100644 --- a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj @@ -9,8 +9,8 @@ https://github.com/teghoz/CustomAutoAdapterMapper README.md license.txt - 1.0.6 - v1.0.6: Bug fixes and improvements. + 1.0.7 + v1.0.7: Fixed collection duplication issue when using mapped ItemKey in update mode. https://github.com/teghoz/CustomAutoAdapterMapper From 84dfd99cf699681699e7b0d8482e9a8d065046a9 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Sat, 10 Jan 2026 11:15:51 -0330 Subject: [PATCH 07/10] Feature/iteration decision making (#14) * Check if itemkey is mapped and check is the objects has the mapped value when the supplied itemkey is not present * updated package version * bug fix for iteration decision * Bug Fix * version update --------- Co-authored-by: Aghogho Bernard --- CustomAutoAdapterMapper/Mapper.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 429ba6b..3a92d0b 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -50,21 +50,13 @@ private static bool JsonStringIsValid(string jsonString) private static bool MapperShouldIterateThroughEntireIncomingCollection(List destination, Option options) { + // First check if the ItemKey property exists and has values in destination var itemKeyIdentifierIsEmpty = destination != null && !string.IsNullOrEmpty(options.ItemKey) && destination.All(x => string.IsNullOrEmpty(x.GetType()?.GetProperty(options.ItemKey) ?.GetValue(x)?.ToString() ?? string.Empty)); - if (itemKeyIdentifierIsEmpty && options.Mappings.ContainsKey(options.ItemKey)) - { - var mappedPropertyName = options.Mappings[options.ItemKey]; - itemKeyIdentifierIsEmpty = destination.All(x => - string.IsNullOrEmpty(x.GetType()?.GetProperty(mappedPropertyName) - ?.GetValue(x)?.ToString() ?? string.Empty)); - options.ItemKey = mappedPropertyName; - } - var result = destination == null || destination.Count == 0 || itemKeyIdentifierIsEmpty; return result; } @@ -108,14 +100,24 @@ private static void SeedKnownCollectionOfItem(List entries, T entry, if (string.IsNullOrEmpty(mapperOptions.ItemKey)) throw new ItemKeyOptionNullException("Item Key Option Not Set!!!!!"); - var keyItemExist = entry.GetType().GetProperty(mapperOptions.ItemKey); + // Determine the JSON key to search for (original or mapped) + var jsonItemKey = mapperOptions.ItemKey; + var objectItemKey = mapperOptions.ItemKey; + + // If ItemKey is mapped, use the mapping for JSON lookup but keep original for object property + if (mapperOptions.Mappings.ContainsKey(mapperOptions.ItemKey)) + { + jsonItemKey = mapperOptions.Mappings[mapperOptions.ItemKey]; + } + + var keyItemExist = entry.GetType().GetProperty(objectItemKey); if (keyItemExist == null) return; var propertyKeyItemValue = - entry.GetType()?.GetProperty(mapperOptions.ItemKey)?.GetValue(entry)?.ToString() ?? null; + entry.GetType()?.GetProperty(objectItemKey)?.GetValue(entry)?.ToString() ?? null; if (propertyKeyItemValue == null) return; var incomingRecord = entries - .Where(e => e.Values().Any(ee => ee.ToString() == propertyKeyItemValue)) + .Where(e => e.SelectToken(jsonItemKey)?.ToString() == propertyKeyItemValue) .FirstOrDefault(); if (incomingRecord == null) return; From ec753a6c222f9be6259a003c93b545dde16833d3 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Sat, 10 Jan 2026 11:21:19 -0330 Subject: [PATCH 08/10] version increment --- CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj index 1d141a4..b1a91cf 100644 --- a/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj +++ b/CustomAutoAdapterMapper/CustomAutoAdapterMapper.csproj @@ -9,8 +9,8 @@ https://github.com/teghoz/CustomAutoAdapterMapper README.md license.txt - 1.0.7 - v1.0.7: Fixed collection duplication issue when using mapped ItemKey in update mode. + 1.0.8 + v1.0.8: Fixed ItemKey mapping logic to correctly identify update vs create mode, preventing duplicate records in collections. https://github.com/teghoz/CustomAutoAdapterMapper From 83a6c69be5cdb07d8ad0eec76a549fd40092eb7e Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Tue, 14 Apr 2026 16:17:57 -0230 Subject: [PATCH 09/10] Support dot-notation traversal for nested root array in RootKey (#16) Switch jsonObject[option.RootKey] to jsonObject.SelectToken(option.RootKey) so that paths like 'getemployee.employeeDetails' resolve correctly into nested JSON structures. Single-level RootKey values continue to work unchanged. Add test coverage for two-level and three-level nested root keys, null mapped property values, missing feed records, and remapped ItemKey JSON lookups. --- CustomAutoAdapterMapper.Tests/UnitTest1.cs | 108 +++++++++++++++++++++ CustomAutoAdapterMapper/Mapper.cs | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/CustomAutoAdapterMapper.Tests/UnitTest1.cs b/CustomAutoAdapterMapper.Tests/UnitTest1.cs index fb42cb5..2d7e236 100644 --- a/CustomAutoAdapterMapper.Tests/UnitTest1.cs +++ b/CustomAutoAdapterMapper.Tests/UnitTest1.cs @@ -260,4 +260,112 @@ public void TestMapperReturnsCollectionWithMappingConfigurationCorrectWithNested Assert.That(result, Is.EqualTo(destinationCollection)); Assert.That(firstItem.DescriptionVariation, Is.Not.Null); } + + [Test] + public void TestMapperSupportsNestedRootKeyWithDotNotation() + { + var json = @" + { + 'data': { + 'employees': [ + { 'API': 'ServiceA', 'Description': 'First', 'Auth': 'apiKey', 'HTTPS': true, 'Cors': 'yes', 'Link': 'https://a.com', 'Category': 'Test' }, + { 'API': 'ServiceB', 'Description': 'Second', 'Auth': 'none', 'HTTPS': false, 'Cors': 'no', 'Link': 'https://b.com', 'Category': 'Test' } + ] + } + }"; + + var destinationCollection = new List(); + var result = json.MapCollection(destinationCollection, options => + { + options.RootKey = "data.employees"; + }); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result.First().API, Is.EqualTo("ServiceA")); + } + + [Test] + public void TestMapperSupportsThreeLevelNestedRootKeyWithDotNotation() + { + var json = @" + { + 'response': { + 'data': { + 'employees': [ + { 'API': 'ServiceA', 'Description': 'First', 'Auth': 'apiKey', 'HTTPS': true, 'Cors': 'yes', 'Link': 'https://a.com', 'Category': 'Test' } + ] + } + } + }"; + + var destinationCollection = new List(); + var result = json.MapCollection(destinationCollection, options => + { + options.RootKey = "response.data.employees"; + }); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().API, Is.EqualTo("ServiceA")); + } + + [Test] + public void TestMapperDoesNotSetMappedPropertyWhenSourceFieldAbsentFromJson() + { + var json = @"{'entries': [{'API': 'ServiceA', 'Description': 'First', 'Auth': 'none', 'HTTPS': true, 'Cors': 'yes', 'Link': 'https://a.com', 'Category': 'Test'}]}"; + + var destinationCollection = new List(); + var result = json.MapCollection(destinationCollection, options => + { + options.RootKey = "entries"; + options.Mappings = new Dictionary + { + { "DescriptionVariation", "NonExistentField" } + }; + }); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().DescriptionVariation, Is.Null); + } + + [Test] + public void TestSeedKnownCollectionReturnsEarlyWhenNoMatchingRecordFoundInFeed() + { + var json = @"{'entries': [{'API': 'ServiceA', 'Description': 'First', 'Auth': 'apiKey', 'HTTPS': true, 'Cors': 'yes', 'Link': 'https://a.com', 'Category': 'Test'}]}"; + + var destinationCollection = new List { new TestObject { API = "NotInFeed" } }; + var result = json.MapCollection(destinationCollection, options => + { + options.RootKey = "entries"; + options.ItemKey = "API"; + options.Mappings = new Dictionary + { + { "Description", "Description" } + }; + }); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().API, Is.EqualTo("NotInFeed")); + Assert.That(result.First().Description, Is.Null); + } + + [Test] + public void TestSeedKnownCollectionUsesRemappedJsonKeyWhenItemKeyIsMapped() + { + var json = @"{'entries': [{'api_name': 'ServiceA', 'Description': 'First', 'Auth': 'apiKey', 'HTTPS': true, 'Cors': 'yes', 'Link': 'https://a.com', 'Category': 'Test'}]}"; + + var destinationCollection = new List { new TestObject { API = "ServiceA" } }; + var result = json.MapCollection(destinationCollection, options => + { + options.RootKey = "entries"; + options.ItemKey = "API"; + options.Mappings = new Dictionary + { + { "API", "api_name" }, + { "Description", "Description" } + }; + }); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().Description, Is.EqualTo("First")); + } } \ No newline at end of file diff --git a/CustomAutoAdapterMapper/Mapper.cs b/CustomAutoAdapterMapper/Mapper.cs index 3a92d0b..e53be92 100644 --- a/CustomAutoAdapterMapper/Mapper.cs +++ b/CustomAutoAdapterMapper/Mapper.cs @@ -148,7 +148,7 @@ private static JToken Validate(string jsonResponse, JObject jsonObject, Option o jsonObject = JObject.Parse(jsonResponse); - var rootProperty = jsonObject[option.RootKey]; + var rootProperty = jsonObject.SelectToken(option.RootKey); if (rootProperty == null) throw new RootKeyPropertyNullException("Root Property Does Not Exist In Object!!!!!"); From 8f6248a282560779f3fcb378bbfeac9662ba5995 Mon Sep 17 00:00:00 2001 From: Aghogho Bernard Date: Tue, 14 Apr 2026 16:27:37 -0230 Subject: [PATCH 10/10] fix dot notation for custom mapping (#18) * use selectToken * updates --------- Co-authored-by: Aghogho Bernard