From 509b5044675e9e275be09a8605937492b86bb462 Mon Sep 17 00:00:00 2001 From: alexmo16 Date: Mon, 5 May 2025 14:18:51 -0400 Subject: [PATCH 1/5] Dictionaries supported within DefaultExpansionsBuilder. --- .../Expansions/DefaultExpansionsBuilder.cs | 7 +++++++ .../ExpansionTests.cs | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs b/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs index 22f6467..141efd9 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs @@ -51,6 +51,13 @@ public void Build(List memberSelectors, ParameterExpression pa if (LogicBuilder.Expressions.Utils.TypeExtensions.IsLiteralType(memberType)) continue; + if (memberType.IsGenericType) + { + Type genericTypeDef = memberType.GetGenericTypeDefinition(); + if (genericTypeDef == typeof(IDictionary<,>) || genericTypeDef == typeof(Dictionary<,>)) + continue; + } + if (LogicBuilder.Expressions.Utils.TypeExtensions.IsList(memberType) && LogicBuilder.Expressions.Utils.TypeExtensions.IsLiteralType ( diff --git a/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs b/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs index 0fcbcec..5ac2da6 100644 --- a/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs +++ b/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs @@ -42,6 +42,26 @@ void Test(ICollection collection) } } + [Fact] + public async Task GetVinylRecordsExpandsComplexTypesByDefault() + { + string query = "/vinylrecordmodel"; + Test(await GetAsync(query)); + + void Test(ICollection collection) + { + Assert.True(collection.Count > 0); + + //Navigation properties + Assert.True(collection.All(vinyl => vinyl.Person is null)); + Assert.True(collection.All(vinyl => vinyl.PressingDetail is null)); + + //Complex types + Assert.Contains(collection, vinyl => vinyl.Properties.Count != 0); + Assert.Contains(collection, vinyl => vinyl.DynamicVinylRecordProperties.Count != 0); + } + } + [Fact] public async Task GetRecordStoresExpandsComplexTypesByDefault() { From 2807a59685819da6765edb565c1017360c122878 Mon Sep 17 00:00:00 2001 From: alexmo16 Date: Wed, 7 May 2025 10:17:14 -0400 Subject: [PATCH 2/5] Handle dictionaries the same way lists are handled. Add an implementation of GetUnderlyingElementType inside AutoMapper.AspNet.OData instead of using the one from LogicBuilder.Expressions.Utils. --- .../Expansions/DefaultExpansionsBuilder.cs | 15 +++-------- .../TypeExtensions.cs | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs b/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs index 141efd9..42ef037 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/Expansions/DefaultExpansionsBuilder.cs @@ -50,24 +50,17 @@ public void Build(List memberSelectors, ParameterExpression pa if (LogicBuilder.Expressions.Utils.TypeExtensions.IsLiteralType(memberType)) continue; - - if (memberType.IsGenericType) - { - Type genericTypeDef = memberType.GetGenericTypeDefinition(); - if (genericTypeDef == typeof(IDictionary<,>) || genericTypeDef == typeof(Dictionary<,>)) - continue; - } - + if (LogicBuilder.Expressions.Utils.TypeExtensions.IsList(memberType) && LogicBuilder.Expressions.Utils.TypeExtensions.IsLiteralType ( - LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(memberType) + TypeExtensions.GetUnderlyingElementType(memberType) ) == false) { List childMemberSelectors = []; ParameterExpression childParam = Expression.Parameter ( - LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(memberType), + TypeExtensions.GetUnderlyingElementType(memberType), GetChildParameterName(param.Name) ); @@ -83,7 +76,7 @@ public void Build(List memberSelectors, ParameterExpression pa ( typeof(Enumerable), "Select", - [LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(selector), typeof(object)], + [TypeExtensions.GetUnderlyingElementType(selector), typeof(object)], selector, childSelector ), diff --git a/AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs b/AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs index d0d78d8..6932a42 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace AutoMapper.AspNet.OData @@ -102,6 +103,31 @@ List DoLoad(List allTypes) public static Dictionary GetEdmToClrTypeMappings() => Constants.EdmToClrTypeMappings; + public static Type GetUnderlyingElementType(this Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + if (typeInfo.IsArray) + return typeInfo.GetElementType(); + + if (!type.IsGenericType) + throw new ArgumentException(nameof(type)); + + Type[] genericArguments = type.GetGenericArguments(); + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + + if (genericTypeDefinition == typeof(IGrouping<,>)) + return genericArguments[1]; + else if (typeof(IDictionary<,>).IsAssignableFrom(genericTypeDefinition)) + return typeof(KeyValuePair<,>).MakeGenericType(genericArguments[0], genericArguments[1]); + else if (genericArguments.Length == 1) + return genericArguments[0]; + else + throw new ArgumentException(nameof(type)); + } + + public static Type GetUnderlyingElementType(this Expression expression) + => GetUnderlyingElementType(expression.Type); + private class AssemblyResolver : IAssemblyResolver { private List _assemblides; From a8f9ab1cd79edf9b11be3841f8110cc48b1b4abe Mon Sep 17 00:00:00 2001 From: alexmo16 Date: Wed, 7 May 2025 11:57:27 -0400 Subject: [PATCH 3/5] Add tests for dictionaries of complex types. --- .../AirVinylData/AirVinylDatabaseInitializer.cs | 8 +++++++- .../AirVinylModel/VinylLinkModel.cs | 7 +++++++ .../AirVinylModel/VinylRecordModel.cs | 17 +++++++++++++++++ AutoMapper.OData.EFCore.Tests/ExpansionTests.cs | 3 +++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylLinkModel.cs diff --git a/AutoMapper.OData.EFCore.Tests/AirVinylData/AirVinylDatabaseInitializer.cs b/AutoMapper.OData.EFCore.Tests/AirVinylData/AirVinylDatabaseInitializer.cs index 1c3ba43..7f15a6e 100644 --- a/AutoMapper.OData.EFCore.Tests/AirVinylData/AirVinylDatabaseInitializer.cs +++ b/AutoMapper.OData.EFCore.Tests/AirVinylData/AirVinylDatabaseInitializer.cs @@ -302,10 +302,16 @@ public static void SeedDatabase(AirVinylDbContext context) context.DynamicVinylRecordProperties.Add(new DynamicProperty() { - VinylRecordId = vinylRecords.First(r => r.Title == "Nevermind").VinylRecordId,//1, + VinylRecordId = vinylRecords.First(r => r.Title == "Nevermind").VinylRecordId,//1 Key = "Publisher", Value = "Geffen" }); + context.DynamicVinylRecordProperties.Add(new DynamicProperty() + { + VinylRecordId = vinylRecords.First(r => r.Title == "Nevermind").VinylRecordId,//1 + Key = "SomeData", + Value = new { TestProp = "value" } + }); context.SaveChanges(); context.DoorManufacturers.AddRange( diff --git a/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylLinkModel.cs b/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylLinkModel.cs new file mode 100644 index 0000000..8ab7e6a --- /dev/null +++ b/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylLinkModel.cs @@ -0,0 +1,7 @@ +namespace AutoMapper.OData.EFCore.Tests.AirVinylModel +{ + public class VinylLinkModel + { + public string Href { get; set; } + } +} diff --git a/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylRecordModel.cs b/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylRecordModel.cs index 52b8f93..93e0d15 100644 --- a/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylRecordModel.cs +++ b/AutoMapper.OData.EFCore.Tests/AirVinylModel/VinylRecordModel.cs @@ -33,5 +33,22 @@ public class VinylRecordModel = new List(); public IDictionary Properties { get; set; } + + private Dictionary _links; + public IDictionary Links { + get + { + if (_links is null) + { + _links = new Dictionary() + { + { "buyingLink", new VinylLinkModel { Href = $"http://test/buy/{VinylRecordId}" } }, + { "reviewLink", new VinylLinkModel { Href = $"http://test/review/{VinylRecordId}" } } + }; + } + + return _links; + } + } } } diff --git a/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs b/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs index 5ac2da6..01a30b9 100644 --- a/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs +++ b/AutoMapper.OData.EFCore.Tests/ExpansionTests.cs @@ -1,6 +1,7 @@ using AutoMapper.AspNet.OData; using AutoMapper.OData.EFCore.Tests.AirVinylData; using AutoMapper.OData.EFCore.Tests.AirVinylModel; +using LogicBuilder.Expressions.Utils; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Query; @@ -58,7 +59,9 @@ void Test(ICollection collection) //Complex types Assert.Contains(collection, vinyl => vinyl.Properties.Count != 0); + Assert.Contains(collection, vinyl => vinyl.Properties.Any(p => !p.Value.GetType().IsLiteralType())); Assert.Contains(collection, vinyl => vinyl.DynamicVinylRecordProperties.Count != 0); + Assert.Contains(collection, vinyl => vinyl.Links.Count != 0); } } From 2f3853df22f682054502f26b005f42182740ac6e Mon Sep 17 00:00:00 2001 From: alexmo16 Date: Wed, 7 May 2025 12:06:12 -0400 Subject: [PATCH 4/5] fix configuration validation test --- AutoMapper.OData.EFCore.Tests/Mappings/AirVinylMappings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/AutoMapper.OData.EFCore.Tests/Mappings/AirVinylMappings.cs b/AutoMapper.OData.EFCore.Tests/Mappings/AirVinylMappings.cs index 01e716a..748dc64 100644 --- a/AutoMapper.OData.EFCore.Tests/Mappings/AirVinylMappings.cs +++ b/AutoMapper.OData.EFCore.Tests/Mappings/AirVinylMappings.cs @@ -30,6 +30,7 @@ public AirVinylMappings() CreateMap() .ForAllMembers(o => o.ExplicitExpansion()); CreateMap() + .ForMember(dest => dest.Links, o => o.Ignore()) .ForAllMembers(o => o.ExplicitExpansion()); } } From d2a4a3f36b106f4cb5db1ac24077afc3f1f5544d Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Fri, 9 May 2025 06:23:09 -0400 Subject: [PATCH 5/5] Staying with the previously referenced LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType in LinqExtensions. --- AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs b/AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs index b8948de..bf0f88d 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs @@ -398,7 +398,7 @@ public static Expression GetSkipCall(this Expression expression, int? skip) ( expression.Type.IsIQueryable() ? typeof(Queryable) : typeof(Enumerable), "Skip", - new[] { expression.GetUnderlyingElementType() }, + new[] { LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(expression) }, expression, Expression.Constant(skip.Value) ); @@ -412,7 +412,7 @@ public static Expression GetTakeCall(this Expression expression, int? top) ( expression.Type.IsIQueryable() ? typeof(Queryable) : typeof(Enumerable), "Take", - new[] { expression.GetUnderlyingElementType() }, + new[] { LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(expression) }, expression, Expression.Constant(top.Value) ); @@ -433,7 +433,7 @@ public static Expression GetOrderByCountCall(this Expression expression, CountNo public static Expression GetOrderByCountCall(this Expression expression, CountNode countNode, string methodName, ODataQueryContext context, string selectorParameterName = "a") { - Type sourceType = expression.GetUnderlyingElementType(); + Type sourceType = LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(expression); ParameterExpression param = Expression.Parameter(sourceType, selectorParameterName); Expression countSelector; @@ -441,7 +441,7 @@ public static Expression GetOrderByCountCall(this Expression expression, CountNo if (countNode.FilterClause is not null) { string memberFullName = countNode.GetPropertyPath(); - Type filterType = sourceType.GetMemberInfoFromFullName(memberFullName).GetMemberType().GetUnderlyingElementType(); + Type filterType = LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(sourceType.GetMemberInfoFromFullName(memberFullName).GetMemberType()); LambdaExpression filterExpression = countNode.FilterClause.GetFilterExpression(filterType, context); countSelector = param.MakeSelector(memberFullName).GetCountCall(filterExpression); } @@ -465,7 +465,7 @@ public static Expression GetOrderByCountCall(this Expression expression, CountNo public static Expression GetOrderByCall(this Expression expression, string memberFullName, string methodName, string selectorParameterName = "a") { - Type sourceType = expression.GetUnderlyingElementType(); + Type sourceType = LogicBuilder.Expressions.Utils.TypeExtensions.GetUnderlyingElementType(expression); MemberInfo memberInfo = sourceType.GetMemberInfoFromFullName(memberFullName); return Expression.Call (