diff --git a/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs b/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs new file mode 100644 index 00000000..4e95caa0 --- /dev/null +++ b/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs @@ -0,0 +1,11 @@ +using System; + +namespace Mapster.Enums +{ + public enum ProjectToTypeAutoMapping + { + AllTypes = 0, + WithoutCollections = 1, + OnlyPrimitiveTypes = 2, + } +} diff --git a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs new file mode 100644 index 00000000..5ee11e7a --- /dev/null +++ b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Mapster.Utils +{ + public sealed class TopLevelMemberNameVisitor : ExpressionVisitor + { + public string? MemeberName { get; private set; } + + public override Expression Visit(Expression node) + { + if (node == null) + return null; + switch (node.NodeType) + { + case ExpressionType.MemberAccess: + { + if (string.IsNullOrEmpty(MemeberName)) + MemeberName = ((MemberExpression)node).Member.Name; + + return base.Visit(node); + } + } + + return base.Visit(node); + } + } + + public sealed class QuoteVisitor : ExpressionVisitor + { + public List Quotes { get; private set; } = new(); + + public override Expression Visit(Expression node) + { + if (node == null) + return null; + switch (node.NodeType) + { + case ExpressionType.Quote: + { + Quotes.Add((UnaryExpression)node); + return base.Visit(node); + } + } + + return base.Visit(node); + } + } +} diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 10ad5507..eec16e14 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Mapster.EFCore.Tests.Models; using MapsterMapper; using Microsoft.EntityFrameworkCore; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; namespace Mapster.EFCore.Tests { @@ -67,6 +67,27 @@ public void MapperInstance_From_OrderBy() var last = orderedQuery.Last(); last.LastName.ShouldBe("Olivetto"); } + + [TestMethod] + public void MergeIncludeWhenUsingEFCoreProjectToType() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + var mapsterInstance = new Mapper(); + + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(); + + var first = query.First(); + + first.Enrollments.Count.ShouldBe(1); + first.LastName.ShouldBe("Alexander"); + } } public class StudentDto diff --git a/src/Mapster.EFCore/EFCoreExtensions.cs b/src/Mapster.EFCore/EFCoreExtensions.cs new file mode 100644 index 00000000..4ed597f0 --- /dev/null +++ b/src/Mapster.EFCore/EFCoreExtensions.cs @@ -0,0 +1,96 @@ +using Mapster.Enums; +using Mapster.Models; +using Mapster.Utils; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +namespace Mapster.EFCore +{ + public static class EFCoreExtensions + { + public static IQueryable EFCoreProjectToType(this IQueryable source, + TypeAdapterConfig? config = null, ProjectToTypeAutoMapping autoMapConfig = ProjectToTypeAutoMapping.WithoutCollections) + { + var allInclude = new IncludeVisitor(); + allInclude.Visit(source.Expression); + + if (config == null) + { + config = TypeAdapterConfig.GlobalSettings + .Clone() + .ForType(source.ElementType, typeof(TDestination)) + .Config; + + var mapTuple = new TypeTuple(source.ElementType, typeof(TDestination)); + + TypeAdapterRule rule; + config.RuleMap.TryGetValue(mapTuple, out rule); + + if(rule != null) + { + rule.Settings.ProjectToTypeMapConfig = autoMapConfig; + + foreach (var item in allInclude.IncludeExpression) + { + var find = rule.Settings.Resolvers.Find(x => x.SourceMemberName == item.Key); + if (find != null) + { + find.Invoker = (LambdaExpression)item.Value.Operand; + find.SourceMemberName = null; + } + else + rule.Settings.ProjectToTypeResolvers.TryAdd(item.Key, item.Value); + } + } + } + else + { + config = config.Clone() + .ForType(source.ElementType, typeof(TDestination)) + .Config; + } + + return source.ProjectToType(config); + } + } + + + internal class IncludeVisitor : ExpressionVisitor + { + public Dictionary IncludeExpression { get; protected set; } = new(); + private bool IsInclude(Expression node) => node.Type.Name.StartsWith("IIncludableQueryable"); + + [return: NotNullIfNotNull("node")] + public override Expression Visit(Expression node) + { + if (node == null) + return null; + + switch (node.NodeType) + { + case ExpressionType.Call: + { + if (IsInclude(node)) + { + var QuoteVisiter = new QuoteVisitor(); + QuoteVisiter.Visit(node); + + foreach (var item in QuoteVisiter.Quotes) + { + var memberv = new TopLevelMemberNameVisitor(); + memberv.Visit(item); + + IncludeExpression.TryAdd(memberv.MemeberName, item); + } + } + return base.Visit(node); + } + } + + return base.Visit(node); + } + } + +} diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index faa490ec..dd0ee95e 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -40,6 +40,63 @@ from src in sources select fn(src, destinationMember, arg)) .FirstOrDefault(result => result != null); + if (arg.MapType == MapType.Projection && getter != null) + { + var s = new TopLevelMemberNameVisitor(); + + s.Visit(getter); + + var match = arg.Settings.ProjectToTypeResolvers.GetValueOrDefault(s.MemeberName); + + if (match != null) + { + arg.Settings.Resolvers.Add(new InvokerModel + { + Condition = null, + DestinationMemberName = destinationMember.Name, + Invoker = (LambdaExpression)match.Operand, + SourceMemberName = null, + IsChildPath = false + + }); + } + + getter = (from fn in resolvers + from src in sources + select fn(src, destinationMember, arg)) + .FirstOrDefault(result => result != null); + } + + + if (arg.MapType == MapType.Projection) + { + + var checkgetter = (from fn in resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains) + from src in sources + select fn(src, destinationMember, arg)) + .FirstOrDefault(result => result != null); + + if (checkgetter == null) + { + Type destinationType; + + if (destinationMember.Type.IsNullable()) + destinationType = destinationMember.Type.GetGenericArguments()[0]; + else + destinationType = destinationMember.Type; + + if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.OnlyPrimitiveTypes + && destinationType.IsMapsterPrimitive() == false) + continue; + + if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.WithoutCollections + && destinationType.IsCollectionCompatible() == true) + continue; + } + + } + + var nextIgnore = arg.Settings.Ignore.Next((ParameterExpression)source, (ParameterExpression?)destination, destinationMember.Name); var nextResolvers = arg.Settings.Resolvers.Next(arg.Settings.Ignore, (ParameterExpression)source, destinationMember.Name) .ToList(); diff --git a/src/Mapster/Settings/SettingStore.cs b/src/Mapster/Settings/SettingStore.cs index e89c776e..821b8569 100644 --- a/src/Mapster/Settings/SettingStore.cs +++ b/src/Mapster/Settings/SettingStore.cs @@ -25,6 +25,12 @@ public void Set(string key, object? value) _objectStore[key] = value; } + + public T GetEnum(string key, Func initializer) where T : System.Enum + { + return (T)_objectStore.GetOrAdd(key, _ => initializer()); + } + public bool? Get(string key) { return _booleanStore.TryGetValue(key, out var value) ? value : null; diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 818a09b5..9158f3a4 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -1,4 +1,5 @@ -using Mapster.Models; +using Mapster.Enums; +using Mapster.Models; using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -105,6 +106,19 @@ public bool? MapToTargetPrimitive set => Set(nameof(MapToTargetPrimitive), value); } + public ProjectToTypeAutoMapping ProjectToTypeMapConfig + { + get => GetEnum(nameof(ProjectToTypeMapConfig), ()=> default(ProjectToTypeAutoMapping)); + set => Set(nameof(ProjectToTypeMapConfig), value); + } + + public Dictionary ProjectToTypeResolvers + { + get => Get(nameof(ProjectToTypeResolvers), () => new Dictionary()); + set => Set(nameof(ProjectToTypeResolvers), value); + } + + public List> ShouldMapMember { get => Get(nameof(ShouldMapMember), () => new List>()); diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index dae413d8..4acad468 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -38,7 +38,7 @@ public static Type GetTypeInfo(this Type type) public static bool IsMapsterPrimitive(this Type type) { - return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string); + return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string) || type.IsEnum; } public static bool IsNullable(this Type type)