diff --git a/src/Mapster.Core/Enums/MapType.cs b/src/Mapster.Core/Enums/MapType.cs index fa8762f1..8c0646bc 100644 --- a/src/Mapster.Core/Enums/MapType.cs +++ b/src/Mapster.Core/Enums/MapType.cs @@ -9,5 +9,6 @@ public enum MapType MapToTarget = 2, Projection = 4, ApplyNullPropagation = 8, + CtorParam = 16, } } \ No newline at end of file diff --git a/src/Mapster.EF6/TypeAdapterBuilderExtensions.cs b/src/Mapster.EF6/TypeAdapterBuilderExtensions.cs index 847d2068..70dfee7b 100644 --- a/src/Mapster.EF6/TypeAdapterBuilderExtensions.cs +++ b/src/Mapster.EF6/TypeAdapterBuilderExtensions.cs @@ -60,10 +60,10 @@ public static ITypeAdapterBuilder EntityFromContext(this IType var getters = keys.Select(key => arg.DestinationType.GetProperty(key)) .Select(prop => new PropertyModel(prop)) .Select(model => arg.Settings.ValueAccessingStrategies - .Select(s => s(src, model, arg)) + .Select(s => s((ResolverSourceInput)src, model, arg)) .FirstOrDefault(exp => exp != null)) .Where(exp => exp != null) - .Select(exp => Expression.Convert(exp, typeof(object))) + .Select(exp => Expression.Convert(exp.Exp, typeof(object))) .ToArray(); if (getters.Length != keys.Length) throw new InvalidOperationException($"Cannot get key for sourceType={arg.SourceType.Name}, destinationType={arg.DestinationType.Name}"); diff --git a/src/Mapster.EFCore/TypeAdapterBuilderExtensions.cs b/src/Mapster.EFCore/TypeAdapterBuilderExtensions.cs index f417651c..88a207a5 100644 --- a/src/Mapster.EFCore/TypeAdapterBuilderExtensions.cs +++ b/src/Mapster.EFCore/TypeAdapterBuilderExtensions.cs @@ -66,10 +66,10 @@ public static ITypeAdapterBuilder EntityFromContext(this IType var getters = keys.Select(key => arg.DestinationType.GetProperty(key)) .Select(prop => new PropertyModel(prop!)) .Select(model => arg.Settings.ValueAccessingStrategies - .Select(s => s(src, model, arg)) + .Select(s => s((ResolverSourceInput)src, model, arg)) .FirstOrDefault(exp => exp != null)) .Where(exp => exp != null) - .Select(exp => Expression.Convert(exp, typeof(object))) + .Select(exp => Expression.Convert(exp.Exp, typeof(object))) .ToArray(); if (getters.Length != keys.Length) throw new InvalidOperationException($"Cannot get key for sourceType={arg.SourceType.Name}, destinationType={arg.DestinationType.Name}"); diff --git a/src/Mapster.Tests/WhenMapUsingOverrideTypesSettings.cs b/src/Mapster.Tests/WhenMapUsingOverrideTypesSettings.cs index 0f03a13d..ddacef3a 100644 --- a/src/Mapster.Tests/WhenMapUsingOverrideTypesSettings.cs +++ b/src/Mapster.Tests/WhenMapUsingOverrideTypesSettings.cs @@ -2,6 +2,7 @@ using Shouldly; using System; using System.Collections.Generic; +using System.Linq; namespace Mapster.Tests { @@ -82,8 +83,203 @@ public void UsingDefaultValueIsWorked() resultCDInsaiderReconfig.Data.ShouldBe(35); } + [TestMethod] + public void CustomDefaultValueIsWorkedWhenUsingAsCtorParam() + { + var config = new TypeAdapterConfig(); + + config.ForDestinationType() + .DefaultValue(x => 42); + + config. + NewConfig() + .MapUsing(dest => dest.Data, src => src.Data, cfg => + { + cfg.ReConfigurate() + .DefaultValue(x => 35); + }); + + var src = new NullableIntInsaider() { Data = null }; + + var result = src.Adapt(config); + + result.Data.ShouldBe(35); + } + + [TestMethod] + public void ExtraSourceUsingCustomConfig() + { + var config = new TypeAdapterConfig(); + config.Default.AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + config.NewConfig() + .MapUsing(dest=> dest, src => src.SrcData, cfg => + { + cfg.SkipDestinationTransforms() + .ReConfigurate() + .Map(dest=>dest.Data, src => 42) + .MapUsing(dest => dest.Collection, src => src.Collection, cfg => + { + cfg + .SkipDestinationTransforms(); + }) + ; + }); + + var src = new SourceFlattentInsaider() { SrcData = new() { Value = "Hello" } }; + + var result = src.Adapt(config); + + result.Collection.ShouldBeNull(); + result.Data.ShouldBe(42); + } + + + [TestMethod] + public void ReMapSettersIsWorked() + { + var config = new TypeAdapterConfig(); + config.Default.AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + config.ForDestinationType() + .Ignore(x => x.Value) + .Ignore(x => x.Data); + config.NewConfig() + .ReMap(dest => dest, src => src.SrcData, true); + config.NewConfig() + .ReMap(dest => dest.Data, src => src.Data); + + var src = new SourceFlattentInsaider() { SrcData = new() { Value = "Hello", Data = 42 } }; + var reMapSrc = new RemapMemberMappings { Data = 21, Value = "World" }; + + + + var result = src.Adapt(config); + + result.Collection.ShouldBeNull(); + result.Value.ShouldBe("Hello"); + result.Data.ShouldBe(42); + + var reMapResut = reMapSrc.Adapt(config); + + reMapResut.Data.ShouldBe(21); + reMapResut.Value.ShouldBe(default); + } + + [TestMethod] + public void ApplyPropagantionUsingDeepSrcAnalize() + { + var config = new TypeAdapterConfig(); + + config.NewConfig() + .Map(dest => dest.ProductNames, src => src.Products.Select(x => x.Name).ToArray()); + + config.NewConfig() + .Map(dest => dest.ProductNames, src => src.Products.Select(x => x.Name).ToArray()); + + config.NewConfig() + .Map(dest => dest.Result, src => $"{src.Value1.ToString()}"); + + config.NewConfig() + .Map(dest => dest.Result, src => $"{src.Value1.ToString()}"); + + var src = new Source1004(); + var srcStrings = new NullableStrings(); + + //var str = src.BuildAdapter(config).CreateMapExpression(); + //var str2 = src.BuildAdapter(config).CreateMapExpression(); + + Should.NotThrow(() => + { + src.Adapt(config); + src.Adapt(config); + + srcStrings.Adapt(config); + srcStrings.Adapt(config); + }); + } + #region TestClasses + class Source1004 + { + public Product1004[]? Products { get; set; } + } + + class Product1004 + { + public required string Name { get; set; } + } + + class Destination1004 + { + public string[]? ProductNames { get; set; } + } + + class DestinationCtor1004 + { + public DestinationCtor1004(string[]? productNames) + { + ProductNames = productNames; + } + + public string[]? ProductNames { get; } + } + + public class NullableStrings + { + public string Value1 { get; set; } + + public string Value2 { get; set; } + } + + public class NullableStringsDest + { + public string Result { get; set; } + } + public class NullableStringsDestCtor + { + public NullableStringsDestCtor(string result) + { + Result = result; + } + + public string Result { get;} + } + + public class RemapMemberMappings + { + public int Data { get; set; } + public string Value { get; set; } + } + + public class DestinationFlattentData + { + public int Data { get; set; } + public string Value { get; set; } + public List Collection { get; set; } + } + + public class SourceFlattentData + { + public int Data { get; set; } + public string Value { get; set; } + public List Collection { get; set; } + } + + public class SourceFlattentInsaider + { + public SourceFlattentData SrcData { get; set; } + } + + public class NullableIntCtorParam + { + public NullableIntCtorParam(int? data) + { + Data = data; + } + public int? Data { get; } + } + + public class NullableIntInsaider { public int? Data { get; set; } diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index f5cafc09..cf2c212d 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -208,7 +208,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de /// Not create destination is abstract type if source is null if (arg.DestinationType.IsAbstract) blocks.Add(Expression.IfThen(Expression.Equal(source, Expression.Constant(null, arg.SourceType)), - Expression.Return(label, Expression.Default(arg.DestinationType)))); + Expression.Return(label, arg.DestinationType.CreateDefault(arg)))); //new TDest(); Expression transformedSource = source; @@ -388,6 +388,9 @@ private static Expression InvokeMapping( if (exp == null) return null; + if(arg.MapType == MapType.CtorParam) + return exp; + //projection null is handled by EF if (arg.MapType != MapType.Projection) exp = source.NotNullReturn(exp,arg); @@ -448,9 +451,10 @@ protected virtual Expression CreateInstantiationExpression(Expression source, Ex } } - internal static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null) + internal static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null, MapType? mapTypeCtor = null) { - var mapType = arg.MapType == MapType.MapToTarget && destination == null ? MapType.Map : + var mapType = mapTypeCtor != null ? mapTypeCtor.Value: + arg.MapType == MapType.MapToTarget && destination == null ? MapType.Map : mapping?.UseDestinationValue == true ? MapType.MapToTarget : arg.MapType; var extraParams = new HashSet(); diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 7bcadabe..418270e2 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -25,30 +25,31 @@ protected ClassMapping CreateClassConverter(Expression source, ClassModel classM if (arg.Settings.IgnoreNonMapped == true) IgnoreNonMapped(classModel,arg); - var sources = new List {source}; + var sources = new List {new ResolverSourceInput(source)}; sources.AddRange( - arg.Settings.ExtraSources.Select(src => - src is LambdaExpression lambda - ? lambda.Apply(arg.MapType, source) - : ExpressionEx.PropertyOrFieldPath(source, (string)src))); + arg.Settings.ExtraSources.Select(src => ResolverSourceInput.ConvertFrom(src,source,arg))); foreach (var destinationMember in destinationMembers) { - if (ProcessIgnores(arg, destinationMember, out var ignore) && !ctorMapping) + if (!destinationMember.ShouldMapMember(arg, MemberSide.Destination)) continue; var resolvers = arg.Settings.ValueAccessingStrategies.AsEnumerable(); if (arg.Settings.IgnoreNonMapped == true) resolvers = resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains); - var getter = (from fn in resolvers + var resolver = (from fn in resolvers from src in sources select fn(src, destinationMember, arg)) .FirstOrDefault(result => result != null); - if(getter is MemberExpression mem && mem?.Expression?.Type == source.Type) - { - getter = Expression.PropertyOrField(source, mem.Member.Name); - } + var getter = resolver?.Exp; + var overideSettings = resolver?.Settings; + + if (ProcessIgnores(arg, destinationMember,out var ignore, resolver) && !ctorMapping) + continue; - var test = resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains); + // ReadyToCleanUp + // source in overideSettings is not source in this context + // if (overideSettings != null && getter != null) + // getter = ReplaceOvverideExpressionParam.Replace(getter, source); if (arg.MapType == MapType.Projection && getter != null) { @@ -72,7 +73,7 @@ select fn(src, destinationMember, arg)) getter = (from fn in resolvers from src in sources select fn(src, destinationMember, arg)) - .FirstOrDefault(result => result != null); + .FirstOrDefault(result => result != null)?.Exp; } @@ -104,15 +105,10 @@ select fn(src, destinationMember, arg)) } - 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(); - var overideSettings = arg.Settings.Resolvers - .Where(x => x.DestinationMemberName == destinationMember.Name && x.OvverideSettings != null) - .Select(x=>x.OvverideSettings).FirstOrDefault(); - var propertyModel = new MemberMapping { DestinationMember = destinationMember, @@ -141,9 +137,9 @@ select fn(src, destinationMember, arg)) } if (getter != null) { - propertyModel.Getter = arg.MapType == MapType.Projection - ? getter - : getter.ApplyPropertyNullPropagation(arg); + propertyModel.Getter = arg.MapType == MapType.Projection || ctorMapping + ? getter + : getter.ApplyPropertyNullPropagation(arg, source); properties.Add(propertyModel); } else @@ -213,10 +209,20 @@ protected static bool IsCanUsingDestinationValue(CompileArgument arg, IMemberMod protected static bool ProcessIgnores( CompileArgument arg, - IMemberModel destinationMember, - out IgnoreDictionary.IgnoreItem ignore) + IMemberModel destinationMember, + out IgnoreDictionary.IgnoreItem ignore, + ResolverResult? resolver = null) { ignore = new IgnoreDictionary.IgnoreItem(); + + if (resolver?.Settings != null) + { + if(resolver.Settings.ReMapExtraSource.GetValueOrDefault() + || resolver.Settings.ReMapDestination.Contains(destinationMember.Name) + || arg.Settings.ReMapDestinationMembers.Contains(destinationMember.Name)) + return false; + } + if (!destinationMember.ShouldMapMember(arg, MemberSide.Destination)) return true; @@ -229,7 +235,8 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var members = classConverter.Members; var arguments = new List(); - arg.Context.NullChecks.UnionWith(members.Where(x => x.Getter != null).Select(x => (x.Getter, arg))); + // ReadyToCleanUp + // arg.Context.NullChecks.UnionWith(members.Where(x => x.Getter != null).Select(x => (x.Getter, arg))); foreach (var member in members) { var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; @@ -275,7 +282,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi } else getter = member.Getter - .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), arg); + .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member,mapTypeCtor:MapType.CtorParam), arg, member); diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 0f2c4bc5..c7e9d253 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -253,8 +253,8 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre if (member.UseDestinationValue) return null; - if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) - && member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member)) + if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) + && contructorMembers.Select(x => x.Name).Contains(member.DestinationMember.Name, new MapsterStringComparer())) continue; if (member.DestinationMember.SetterModifier == AccessModifier.None) @@ -282,7 +282,7 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre static Expression CreateIncludeProjectionExpression(Expression source, CompileArgument arg) { - Expression body = Expression.Default(arg.DestinationType); + Expression body = arg.DestinationType.CreateDefault(arg); foreach (var tuple in arg.Settings.Includes) { var itemTuple = tuple; diff --git a/src/Mapster/Compile/CompileContext.cs b/src/Mapster/Compile/CompileContext.cs index 72d640c8..0c21abc7 100644 --- a/src/Mapster/Compile/CompileContext.cs +++ b/src/Mapster/Compile/CompileContext.cs @@ -12,7 +12,9 @@ public class CompileContext public int? MaxDepth { get; set; } public int Depth { get; set; } public HashSet ExtraParameters { get; } = new(); - public HashSet<(Expression param, CompileArgument arg)> NullChecks { get; } = new(); + + // ReadyToCleanUp + // public HashSet<(Expression param, CompileArgument arg)> NullChecks { get; } = new(); internal bool IsSubFunction() { diff --git a/src/Mapster/Models/ExtraSourceModel.cs b/src/Mapster/Models/ExtraSourceModel.cs new file mode 100644 index 00000000..94469111 --- /dev/null +++ b/src/Mapster/Models/ExtraSourceModel.cs @@ -0,0 +1,10 @@ +using System.Linq.Expressions; + +namespace Mapster.Models +{ + public record ExtraSourceModel(object Src, OverrideTypesSettings? Settings = null) + { + public static explicit operator ExtraSourceModel(Expression src) => new ExtraSourceModel(src); + public static explicit operator ExtraSourceModel(string src) => new ExtraSourceModel(src); + } +} diff --git a/src/Mapster/Models/InvokerModel.cs b/src/Mapster/Models/InvokerModel.cs index f2201c3d..bad43ecc 100644 --- a/src/Mapster/Models/InvokerModel.cs +++ b/src/Mapster/Models/InvokerModel.cs @@ -1,5 +1,6 @@ -using System.Linq.Expressions; -using Mapster.Utils; +using Mapster.Utils; +using System.Collections.Generic; +using System.Linq.Expressions; namespace Mapster.Models { @@ -31,20 +32,34 @@ public class InvokerModel }; } - public Expression GetInvokingExpression(Expression exp, MapType mapType = MapType.Map) + public Expression GetInvokingExpression(Expression exp, MapType mapType = MapType.Map, bool isExtraParam = false) { if (IsChildPath) return Invoker!.Body; return SourceMemberName != null ? ExpressionEx.PropertyOrFieldPath(exp, SourceMemberName) - : Invoker!.Apply(mapType, exp); + : isExtraParam ? Invoker!.ApplyExtraSources(mapType, exp) : Invoker!.Apply(mapType, exp); } - public Expression? GetConditionExpression(Expression exp, MapType mapType = MapType.Map) + public Expression? GetConditionExpression(Expression exp, MapType mapType = MapType.Map, bool isExtraParam = false) { return IsChildPath ? Condition?.Body - : Condition?.Apply(mapType, exp); + : isExtraParam ? Condition?.ApplyExtraSources(mapType, exp) : Condition?.Apply(mapType, exp); + } + } + + public class InvokerModelApplyComparer : IEqualityComparer + { + public bool Equals(InvokerModel? x, InvokerModel? y) + { + if (x is null || y is null) return false; + return string.Equals(x.DestinationMemberName, y.DestinationMemberName, System.StringComparison.InvariantCulture); + } + + public int GetHashCode(InvokerModel obj) + { + return obj?.DestinationMemberName?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index a94c7d79..c045d750 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -5,7 +5,7 @@ using System.Reflection; using Mapster.Models; using Mapster.Utils; -using ValueAccess = System.Func; +using ValueAccess = System.Func; namespace Mapster { @@ -24,12 +24,14 @@ public static class ValueAccessingStrategy CustomResolverForDictionary, }; - private static Expression? CustomResolverFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? CustomResolverFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; var config = source.Type == arg.SourceType ? arg.Settings : arg.Context.Config.GetMergedSettings(new TypeTuple(source.Type, arg.DestinationType),arg.MapType); - var resolvers = config.Resolvers; + var resolvers = srcInput.Settings != null ? srcInput.Settings.ApplyResolversOnly(config) : config.Resolvers; if (resolvers.Count == 0) return null; + TypeAdapterSettings? customSettings = null; var invokes = new List>(); @@ -39,7 +41,10 @@ public static class ValueAccessingStrategy if (!destinationMember.Name.Equals(resolver.DestinationMemberName, StringComparison.InvariantCultureIgnoreCase)) continue; - var invoke = resolver.GetInvokingExpression(source, arg.MapType); + if(resolver.OvverideSettings != null && customSettings == null) + customSettings = resolver.OvverideSettings; + + var invoke = resolver.GetInvokingExpression(source, arg.MapType, customSettings != null); var condition = resolver.GetConditionExpression(source, arg.MapType); if (condition == null) { @@ -66,23 +71,33 @@ public static class ValueAccessingStrategy } } - return getter; + if (getter == null) + return null; + return new ResolverResult(getter,(OverrideTypesSettings?)customSettings); } - private static Expression? PropertyOrFieldFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? PropertyOrFieldFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; var members = source.Type.GetFieldsAndProperties(true); var strategy = arg.Settings.NameMatchingStrategy; var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); - return members + var resolver = members .Where(member => member.ShouldMapMember(arg, MemberSide.Source)) .Where(member => member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg) == destinationMemberName) .Select(member => member.GetExpression(source)) .FirstOrDefault(); + + if (resolver == null) + return null; + else + return new ResolverResult(resolver, srcInput.Settings != null ? srcInput.Settings.CloneOnlySkipSettings() : null); + } - private static Expression? GetMethodFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? GetMethodFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; if (arg.MapType == MapType.Projection) return null; var strategy = arg.Settings.NameMatchingStrategy; @@ -92,14 +107,18 @@ public static class ValueAccessingStrategy return null; if (getMethod.Name == "GetType" && destinationMember.Type != typeof(Type)) return null; - return Expression.Call(source, getMethod); + return new ResolverResult( Expression.Call(source, getMethod),null); } - private static Expression? FlattenMemberFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? FlattenMemberFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; var strategy = arg.Settings.NameMatchingStrategy; var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); - return GetDeepFlattening(source, destinationMemberName, arg); + var resolver = GetDeepFlattening(source, destinationMemberName, arg); + if(resolver == null) + return null; + return new ResolverResult(resolver, null); } private static Expression? GetDeepFlattening(Expression source, string propertyName, CompileArgument arg) @@ -177,8 +196,9 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM } } - private static Expression? DictionaryFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? DictionaryFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; var dictType = source.Type.GetDictionaryType(); if (dictType == null) return null; @@ -192,19 +212,22 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM var method = typeof(MapsterHelper).GetMethods() .First(m => m.Name == nameof(MapsterHelper.FlexibleGet) && m.GetParameters()[0].ParameterType.Name == dictType.Name) .MakeGenericMethod(args[1]); - return Expression.Call(method, source.To(dictType), key, ExpressionEx.GetNameConverterExpression(strategy.SourceMemberNameConverter)); + var resolver = Expression.Call(method, source.To(dictType), key, ExpressionEx.GetNameConverterExpression(strategy.SourceMemberNameConverter)); + return new ResolverResult(resolver); } else { var method = typeof(MapsterHelper).GetMethods() .First(m => m.Name == nameof(MapsterHelper.GetValueOrDefault) && m.GetParameters()[0].ParameterType.Name == dictType.Name) .MakeGenericMethod(args); - return Expression.Call(method, source.To(dictType), key); + var resolver = Expression.Call(method, source.To(dictType), key); + return new ResolverResult(resolver); } } - private static Expression? CustomResolverForDictionaryFn(Expression source, IMemberModel destinationMember, CompileArgument arg) + private static ResolverResult? CustomResolverForDictionaryFn(ResolverSourceInput srcInput, IMemberModel destinationMember, CompileArgument arg) { + var source = srcInput.Src; var config = arg.Settings; var resolvers = config.Resolvers; if (resolvers.Count == 0) @@ -236,7 +259,21 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM } if (lastCondition != null) getter = Expression.Condition(lastCondition, getter!, getter!.Type.CreateDefault(arg)); - return getter; + return new ResolverResult(getter); } } + + public record ResolverResult(Expression Exp , OverrideTypesSettings? Settings = null); + public record ResolverSourceInput(Expression Src, OverrideTypesSettings? Settings = null) + { + public static explicit operator ResolverSourceInput(Expression src) => new ResolverSourceInput(src); + public static explicit operator ResolverSourceInput(ParameterExpression src) => new ResolverSourceInput(src); + public static ResolverSourceInput ConvertFrom(ExtraSourceModel extraSource,Expression source, CompileArgument arg) + { + if (extraSource.Src is LambdaExpression lambda) + return new ResolverSourceInput(lambda.ApplyExtraSources(arg.MapType, source), extraSource.Settings); + else + return new ResolverSourceInput(ExpressionEx.PropertyOrFieldPath(source, (string)extraSource.Src), extraSource.Settings); + } + }; } diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 8758e064..19e4dc64 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -495,7 +495,7 @@ private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpress var condition = Expression.TypeIs(tempDest, destinationType); UnaryExpression ifTrue = Expression.Convert(tempDest, destinationType); - DefaultExpression ifFalse = Expression.Default(destinationType); + Expression ifFalse = destinationType.CreateDefault(arg); ConditionalExpression conditionalExpr = Expression.Condition(condition, ifTrue, ifFalse); blockbody.Add(conditionalExpr); diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index 2577cde5..01d4bbae 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -431,7 +431,7 @@ public TypeAdapterSetter Map( var invoker = Expression.Lambda(source.Body, Expression.Parameter(typeof (object))); if (member.IsIdentity()) { - Settings.ExtraSources.Add(invoker); + Settings.ExtraSources.Add((ExtraSourceModel)invoker); return this; } @@ -452,7 +452,7 @@ public TypeAdapterSetter Map( if (destinationMember.IsIdentity()) { - Settings.ExtraSources.Add(sourceMemberName); + Settings.ExtraSources.Add((ExtraSourceModel)sourceMemberName); return this; } @@ -645,23 +645,62 @@ public TypeAdapterSetter MapUsing(this.Config); + configAction(Tempsetter); + + overrideSettings = Tempsetter.Settings; + } + if (member.IsIdentity()) { - Settings.ExtraSources.Add(invoker); + Settings.ExtraSources.Add(new ExtraSourceModel(invoker, (OverrideTypesSettings?)overrideSettings)); return this; } + Settings.Resolvers.Add(new InvokerModel + { + DestinationMemberName = member.GetMemberPath()!, + Invoker = invoker, + Condition = null, + OvverideSettings = overrideSettings + }); + return this; + } + + public TypeAdapterSetter ReMap( + Expression> member, + Expression> source, + bool SkipDestinationTransforms = false) + { + this.CheckCompiled(); + + + + var invoker = Expression.Lambda(source.Body, Expression.Parameter(typeof(TSource))); TypeAdapterSettings? overrideSettings = null; + + var Tempsetter = new OverrideTypesSetter(this.Config); + overrideSettings = Tempsetter.Settings; + - if (configAction != null) + if (SkipDestinationTransforms) + Tempsetter.SkipDestinationTransforms(); + + if (member.IsIdentity()) { - var Tempsetter = new OverrideTypesSetter(this.Config); - configAction(Tempsetter); + Tempsetter._Settings.ReMapExtraSource = true; - overrideSettings = Tempsetter.Settings; + Settings.ExtraSources.Add(new ExtraSourceModel(invoker, (OverrideTypesSettings?)overrideSettings)); + return this; } + this.Settings.ReMapDestinationMembers.Add(member.GetMemberPath()!); + Settings.Resolvers.Add(new InvokerModel { DestinationMemberName = member.GetMemberPath()!, @@ -710,7 +749,7 @@ public TypeAdapterSetter Map (OverrideTypesSettings)Settings; } + internal protected OverrideTypesSettings _Settings { get => (OverrideTypesSettings)Settings; } public OverrideTypesSetter(TypeAdapterConfig config) : this (new OverrideTypesSettings (), config) { } public OverrideTypesSetter(TypeAdapterSettings settings, TypeAdapterConfig config) : base(settings, config) { } diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index f72a25a6..f03404c1 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -134,17 +134,17 @@ public Dictionary ProjectToTypeResolvers { get => Get(nameof(ShouldMapMember), () => new List>()); } - public List> ValueAccessingStrategies + public List> ValueAccessingStrategies { - get => Get(nameof(ValueAccessingStrategies), () => new List>()); + get => Get(nameof(ValueAccessingStrategies), () => new List>()); } public List Resolvers { get => Get(nameof(Resolvers), () => new List()); } - public List ExtraSources + public List ExtraSources { - get => Get(nameof(ExtraSources), () => new List()); + get => Get(nameof(ExtraSources), () => new List()); } public List> BeforeMappingFactories { @@ -208,6 +208,11 @@ public Expression? CustomDefaultValue set => Set(nameof(CustomDefaultValue), value); } + public List ReMapDestinationMembers + { + get => Get(nameof(ReMapDestinationMembers), () => new List()); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() diff --git a/src/Mapster/TypeAdapterSettings/OverrideTypesSettings.cs b/src/Mapster/TypeAdapterSettings/OverrideTypesSettings.cs index ebb160c4..359b13cf 100644 --- a/src/Mapster/TypeAdapterSettings/OverrideTypesSettings.cs +++ b/src/Mapster/TypeAdapterSettings/OverrideTypesSettings.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using Mapster.Models; +using System; +using System.Collections.Generic; +using System.Linq; namespace Mapster { @@ -10,6 +13,17 @@ public List SkipSettings get => Get(nameof(SkipSettings), () => new List()); } + public IEnumerable ReMapDestination + { + get => this.Resolvers.Select(x=>x.DestinationMemberName).Union(ReMapDestinationMembers); + } + + public bool? ReMapExtraSource + { + get => Get(nameof(ReMapExtraSource)); + set => Set(nameof(ReMapExtraSource), value); + } + public bool? SkipAllSettings { get => Get(nameof(SkipAllSettings)); @@ -27,5 +41,31 @@ public override void Apply(SettingStore other) if(!SkipAllSettings.GetValueOrDefault()) base.ApplyWithSkipSettings(other, SkipSettings); } + + public List ApplyResolversOnly(TypeAdapterSettings other) + { + var result = new List(this.Resolvers); + var seen = new HashSet(result,new InvokerModelApplyComparer()); + + foreach (var item in other.Resolvers) + { + if (seen.Add(item)) + { + result.Add(item); + } + } + return result; + } + + public OverrideTypesSettings CloneOnlySkipSettings() + { + var result = new OverrideTypesSettings(); + + result.SkipAllSettings = this.SkipAllSettings; + result.SkipSettings.AddRange(this.SkipSettings); + result.ReMapExtraSource = this.ReMapExtraSource; + + return result; + } } } diff --git a/src/Mapster/Utils/DirectParameterMemberFinder.cs b/src/Mapster/Utils/DirectParameterMemberFinder.cs new file mode 100644 index 00000000..495055fa --- /dev/null +++ b/src/Mapster/Utils/DirectParameterMemberFinder.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; + +public class DirectParameterMemberFinder : ExpressionVisitor +{ + private readonly bool _isCtrMapping; + private readonly HashSet _TargetParams; + public List FoundMembers { get; } = new(); + + public DirectParameterMemberFinder(bool conctructorMapping = false, params Expression[] targetParams) + { + _TargetParams = new HashSet(targetParams); + _isCtrMapping = conctructorMapping; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (_TargetParams.Contains(GetParametr(node))) + if (_isCtrMapping) + FoundMembers.Add(node); + else + FoundMembers.Add(node.Expression); + + return node; + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Object is MemberExpression mem && _TargetParams.Contains(GetParametr(mem))) + FoundMembers.Add(mem); + + foreach (var arg in node.Arguments) + { + if (arg is MemberExpression member && _TargetParams.Contains(GetParametr(member))) + { + // if Method is static for Type && not Extention method + if (node.Object == null && !node.Method.IsDefined(typeof(ExtensionAttribute), inherit: false)) + FoundMembers.Add(member.Expression); + else + FoundMembers.Add(member); + continue; + } + + if (arg.NodeType == ExpressionType.Call) + { + Visit(arg); + } + } + + return node; + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Convert || node.NodeType == ExpressionType.ConvertChecked) + { + var result = base.VisitUnary(node); + return result; + } + return base.VisitUnary(node); + } + + public IEnumerable Find(Expression expression) + { + FoundMembers.Clear(); + Visit(expression); + + return FoundMembers; + } + + + private Expression GetParametr(MemberExpression member) + { + Expression current = member; + + while (current != null) + { + if (current is MemberExpression mem) + { + current = mem.Expression; + continue; + } + + if (current is ParameterExpression) + return current; + else + current = new ReturnParametrVisitor().GetParam(current); + } + + return Expression.Empty(); + } + + internal class ReturnParametrVisitor : ExpressionVisitor + { + private Expression parametr; + + protected override Expression VisitParameter(ParameterExpression node) + { + parametr = node; + return node; + } + + public Expression GetParam(Expression expression) + { + Visit(expression); + return parametr; + } + } + + +} \ No newline at end of file diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 4b9b0e18..6a768755 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -152,6 +152,11 @@ public static Expression Apply(this LambdaExpression lambda, MapType mapType, pa return lambda.Apply(mapType != MapType.Projection, exps); } + public static Expression ApplyExtraSources(this LambdaExpression lambda, MapType mapType, params Expression[] exps) + { + return lambda.ApplyExtraSources(mapType != MapType.Projection, exps); + } + public static Expression Apply(this LambdaExpression lambda, ParameterExpression p1, ParameterExpression? p2 = null) { if (p2 == null) @@ -169,6 +174,15 @@ private static Expression Apply(this LambdaExpression lambda, bool allowInvoke, return Expression.Invoke(lambda, exps); } + private static Expression ApplyExtraSources(this LambdaExpression lambda, bool allowInvoke, params Expression[] exps) + { + var replacer = new ParameterExpressionReplacer(lambda.Parameters,true, exps); + var result = replacer.Visit(lambda.Body); + if (!allowInvoke || !replacer.ReplaceCounts.Where((n, i) => n > 1 && exps[i].IsComplex()).Any()) + return result!; + return Expression.Invoke(lambda, exps); + } + public static LambdaExpression TrimParameters(this LambdaExpression lambda, int skip = 0) { var replacer = new ParameterExpressionReplacer(lambda.Parameters, lambda.Parameters.ToArray()); @@ -407,7 +421,114 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyPropertyNullPropagation(this Expression getter, CompileArgument arg) + + public static Expression ApplyPropertyNullPropagation(this Expression getter, CompileArgument arg, Expression source) + { + var current = getter; + var result = getter; + Expression? condition = null; + + var finder = new DirectParameterMemberFinder(false,source); + var condition2 = finder.Find(getter) + .Select(x => x.GetNullPropagationChecks(arg)) + .Where(x => x != null) + .ToArray().ConcatPropagationChecks(); + + if (condition2 == null) + return getter; + + if (!getter.Type.CanBeNull()) + { + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition2, transform, transform.Type.CreateDefault()); + } + else + return Expression.Condition(condition2, getter, getter.Type.CreateDefault()); + } + + public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg, MemberMapping mapping) + { + if (getter == null) + return adapt; + + var finder = new DirectParameterMemberFinder(true,mapping.Source); + + Expression? condition = finder.Find(getter) + .Select(x => x.GetNullPropagationChecks(arg)) + .Where(x => x != null) + .ToArray().ConcatPropagationChecks(); + + if (condition == null) + return adapt; + + // add supporting DestinationTransforms + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); + if (transform != null) + return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, adapt.Type.CreateDefault(arg))); + + return Expression.Condition(condition, adapt, adapt.Type.CreateDefault(member: mapping)); + } + + + private static Expression? ConcatPropagationChecks(this Expression[] checks) + { + if (checks.Length == 0) + return null; + + if (checks.Length == 1) + return checks.First(); + + Expression? result = null; + + for (int i = 0; i < checks.Length; i++) + { + if (i == 0) + result = checks[i]; + else + { + result = Expression.AndAlso(result, checks[i]); + } + + } + + return result; + } + + private static Expression? GetNullPropagationChecks (this Expression getter, CompileArgument arg) + { + Expression? condition = null; + var current = getter; + + while (current != null) + { + Expression? compareNull = null; + + if (current.Type.CanBeNull() && current is not ParameterExpression) + compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + + else if (current.Type.CanBeNull() && current is ParameterExpression param + && arg.MapType == MapType.Projection) + + compareNull = Expression.NotEqual(param, Expression.Constant(null, param.Type)); + + if (compareNull != null) + { + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); + } + + if (current is MemberExpression member) + current = member.Expression; + else + current = null; + } + + return condition; + } + + public static Expression ApplyPropertyNullPropagationLegasy(this Expression getter, CompileArgument arg) { var current = getter; var result = getter; @@ -415,7 +536,7 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Co while (current.NodeType == ExpressionType.MemberAccess) { - var memEx = (MemberExpression) current; + var memEx = (MemberExpression)current; var expr = memEx.Expression; if (expr == null) break; @@ -424,10 +545,10 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Co if (!getter.CanBeNull()) { var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); - return Expression.Condition(condition, transform, transform.Type.CreateDefault(arg)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); } else - return Expression.Condition(condition, getter, getter.Type.CreateDefault(arg)); + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); } if (expr.CanBeNull()) @@ -445,16 +566,17 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Co return getter; } - public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) + public static Expression ApplyNullPropagationFromCtorLegasy(this Expression getter, Expression adapt, CompileArgument arg, MemberMapping mapping) { if (getter == null) return adapt; Expression? condition = null; var current = getter; - var checks = arg.Context.NullChecks - .Where(x => !object.ReferenceEquals(x.arg, arg)) - .Select(x => x.param); + // ReadyToCleanUp + //var checks = arg.Context.NullChecks + // .Where(x => !object.ReferenceEquals(x.arg, arg)) + // .Select(x => x.param); while (current != null) { @@ -462,8 +584,12 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex if (current.CanBeNull() && current is not ParameterExpression) compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + // ReadyToCleanUp + //else if (current.CanBeNull() && current is ParameterExpression param + // && !checks.Contains(param)) else if (current.CanBeNull() && current is ParameterExpression param - && !checks.Contains(param)) + && arg.MapType == MapType.Projection) + compareNull = Expression.NotEqual(param, Expression.Constant(null, param.Type)); if (compareNull != null) @@ -486,9 +612,9 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex // add supporting DestinationTransforms var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); if (transform != null) - return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, Expression.Default(adapt.Type))); + return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, adapt.Type.CreateDefault(arg))); - return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); + return Expression.Condition(condition, adapt, adapt.Type.CreateDefault(member:mapping)); } public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) diff --git a/src/Mapster/Utils/ParameterExpressionReplacer.cs b/src/Mapster/Utils/ParameterExpressionReplacer.cs index 16c02465..f1a0ee91 100644 --- a/src/Mapster/Utils/ParameterExpressionReplacer.cs +++ b/src/Mapster/Utils/ParameterExpressionReplacer.cs @@ -8,10 +8,20 @@ sealed class ParameterExpressionReplacer : ExpressionVisitor //fields readonly ReadOnlyCollection _from; readonly Expression[] _to; + readonly bool _FromExtraSource; public int[] ReplaceCounts { get; } //constructors + + public ParameterExpressionReplacer(ReadOnlyCollection from,bool isExtraSource, params Expression[] to ) + { + _from = from; + _to = to; + ReplaceCounts = new int[_to.Length]; + _FromExtraSource = isExtraSource; + } + public ParameterExpressionReplacer(ReadOnlyCollection from, params Expression[] to) { _from = from; @@ -24,7 +34,16 @@ protected override Expression VisitParameter(ParameterExpression node) for (var i = 0; i < _from.Count; i++) { if (node != _from[i]) - continue; + { + if (_FromExtraSource) + { + if (node.Type != _from[i].Type) + continue; + } + else + continue; + } + if (i >= _to.Length) return node.Type.CreateDefault(); diff --git a/src/Mapster/Utils/ParametrExpressionFinder.cs b/src/Mapster/Utils/ParametrExpressionFinder.cs new file mode 100644 index 00000000..c9e0ab13 --- /dev/null +++ b/src/Mapster/Utils/ParametrExpressionFinder.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq.Expressions; + +namespace Mapster.Utils +{ + sealed internal class ParametrExpressionFinder: ExpressionVisitor + { + private readonly List _parameters = new(); + + protected override Expression VisitParameter(ParameterExpression node) + { + if (!_parameters.Contains(node)) + _parameters.Add(node); + + return base.VisitParameter(node); + } + + public ReadOnlyCollection Find(Expression expression) + { + _parameters.Clear(); + this.Visit(expression); + return _parameters.AsReadOnly(); + } + } + + internal static class ReplaceOvverideExpressionParam + { + readonly static ParametrExpressionFinder ParamFinder = new (); + + public static Expression Replace(Expression expression, params Expression[] to) + { + return new ParameterExpressionReplacer(ParamFinder.Find(expression), true, to).Visit(expression); + } + } +} diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index d5051c43..96fc5ca4 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -358,11 +358,15 @@ public static bool IsPrimitiveKind(this Type type) return type == typeof(object) || type.UnwrapNullable().IsConvertible(); } - public static Expression CreateDefault(this Type type, CompileArgument? arg = null) + public static Expression CreateDefault(this Type type, CompileArgument? arg = null, MemberMapping? member = null) { if(arg !=null && arg.Settings.CustomDefaultValue != null) return arg.Settings.CustomDefaultValue; + if (member != null && member.OverrideSettings != null + && member.OverrideSettings.CustomDefaultValue != null) + return member.OverrideSettings.CustomDefaultValue; + return type.CanBeNull() ? Expression.Constant(null, type) : Expression.Constant(Activator.CreateInstance(type), type); diff --git a/src/Mapster/Utils/StringComparer.cs b/src/Mapster/Utils/StringComparer.cs new file mode 100644 index 00000000..e3f0d7f4 --- /dev/null +++ b/src/Mapster/Utils/StringComparer.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Mapster.Utils +{ + internal class MapsterStringComparer : IEqualityComparer + { + public bool Equals(string? x, string? y) + { + if(String.IsNullOrEmpty(x) || String.IsNullOrEmpty(y)) + return false; + + return String.Equals(x, y, StringComparison.InvariantCultureIgnoreCase); + } + + public int GetHashCode(string obj) + { + if(obj is null) + return 0; + + return obj.GetHashCode(); + } + } +}