diff --git a/src/Mapster.Async.Tests/AsyncTest.cs b/src/Mapster.Async.Tests/AsyncTest.cs index 1d85146d..caefe5ce 100644 --- a/src/Mapster.Async.Tests/AsyncTest.cs +++ b/src/Mapster.Async.Tests/AsyncTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using MapsterMapper; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -87,6 +88,22 @@ public async Task NestedAsync() dtoOwnership.Owner.Name.ShouldBe("John Doe"); } + [TestMethod] + public async Task SimplyAsync() + { + TypeAdapterConfig.NewConfig() + .AfterMappingAsync(async dest => { dest.Name = await GetName(); }); + + var poco = new Poco { Id = "foo" }; + var dto = await poco.AdaptAsync(); + dto.Name.ShouldBe("bar"); + + IMapper instance = new Mapper(); + + var destination = await instance.MapAsync(poco); + destination.Name.ShouldBe("bar"); + } + private static async Task GetName() { await Task.Delay(1); diff --git a/src/Mapster.Async/TypeAdapterExtensions.cs b/src/Mapster.Async/TypeAdapterExtensions.cs index b42a754e..6797c1b2 100644 --- a/src/Mapster.Async/TypeAdapterExtensions.cs +++ b/src/Mapster.Async/TypeAdapterExtensions.cs @@ -1,4 +1,5 @@ -using System; +using MapsterMapper; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -101,5 +102,44 @@ public static async Task AdaptToAsync(this IAdapterB } } + + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Type of destination object that mapped. + public static async Task AdaptAsync(this object? source) + { + return await source.BuildAdapter().AdaptToTypeAsync(); + } + + + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Configuration + /// Type of destination object that mapped. + public static async Task AdaptAsync(this object? source, TypeAdapterConfig config) + { + return await source.BuildAdapter(config).AdaptToTypeAsync(); + } + + } + + public static class IMapperAsyncExtentions + { + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Type of destination object that mapped. + public static async Task MapAsync(this IMapper mapper, object? source) + { + return await mapper.From(source).AdaptToTypeAsync(); + } } } 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.Immutable/ImmutableAdapter.cs b/src/Mapster.Immutable/ImmutableAdapter.cs index 9ab7a3a5..596cac4d 100644 --- a/src/Mapster.Immutable/ImmutableAdapter.cs +++ b/src/Mapster.Immutable/ImmutableAdapter.cs @@ -69,7 +69,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster.JsonNet/JsonAdapter.cs b/src/Mapster.JsonNet/JsonAdapter.cs index c44fe773..75691f7a 100644 --- a/src/Mapster.JsonNet/JsonAdapter.cs +++ b/src/Mapster.JsonNet/JsonAdapter.cs @@ -51,7 +51,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio throw new System.NotImplementedException(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new System.NotImplementedException(); } diff --git a/src/Mapster.Tests/Mapster.Tests.csproj b/src/Mapster.Tests/Mapster.Tests.csproj index 1fd66a01..438405ee 100644 --- a/src/Mapster.Tests/Mapster.Tests.csproj +++ b/src/Mapster.Tests/Mapster.Tests.csproj @@ -7,6 +7,7 @@ Mapster.Tests.snk true true + 11.0 diff --git a/src/Mapster.Tests/WhenExplicitMappingRequired.cs b/src/Mapster.Tests/WhenExplicitMappingRequired.cs index b5d89492..25b43474 100644 --- a/src/Mapster.Tests/WhenExplicitMappingRequired.cs +++ b/src/Mapster.Tests/WhenExplicitMappingRequired.cs @@ -13,6 +13,7 @@ public class WhenExplicitMappingRequired public void TestCleanup() { TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = false; + TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = false; TypeAdapterConfig.GlobalSettings.Clear(); } @@ -140,8 +141,60 @@ public void UnmappedChildPocoShouldFailed() setter.Compile(); // Should fail here } + [TestMethod] + public void RequireExplicitMappingPrimitiveWork() + { + TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = true; + + TypeAdapterConfig.NewConfig(); + + Should.Throw(() => + { + TypeAdapterConfig.GlobalSettings.Compile(); // throw CompileException + }); + + byte byteSource = 10; + + byteSource.Adapt(); // Should work when the type is mapped to itself + + Should.Throw(() => + { + byteSource.Adapt(); // throw CompileException, Do not map to another primitive type without registering the configuration + }); + + Should.NotThrow(() => + { + TypeAdapterConfig.NewConfig(); + + byteSource.Adapt(); // Not throw CompileException when config is registering + }); + + Should.NotThrow(() => + { + TypeAdapterConfig.NewConfig() + .Map(dest=> dest.MyProperty, src=> int.Parse(src.MyProperty)); + // it work works because int.Parse return Type Int. Type is mapped to itself (int -> int) without config. + + var sourceMapconfig = new Source783() { MyProperty = "128" }; + var resultMapconfig = sourceMapconfig.Adapt(); + + resultMapconfig.MyProperty.ShouldBe(128); + }); + + } + + #region TestClasses + + public class Source783 + { + public string MyProperty { get; set; } = ""; + } + public class Destination783 + { + public int MyProperty { get; set; } + } public enum NameEnum { diff --git a/src/Mapster.Tests/WhenIgnoreMapping.cs b/src/Mapster.Tests/WhenIgnoreMapping.cs index 585e911c..245c4e63 100644 --- a/src/Mapster.Tests/WhenIgnoreMapping.cs +++ b/src/Mapster.Tests/WhenIgnoreMapping.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Shouldly; @@ -55,6 +57,109 @@ public void TestIgnoreMember() poco2.Name.ShouldBeNull(); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/707 + /// + [TestMethod] + public void WhenClassIgnoreCtorParamGetDefaultValue() + { + var config = new TypeAdapterConfig() + { + RequireDestinationMemberSource = true, + }; + config.Default + .NameMatchingStrategy(new NameMatchingStrategy + { + SourceMemberNameConverter = input => input.ToLowerInvariant(), + DestinationMemberNameConverter = input => input.ToLowerInvariant(), + }) + ; + config + .NewConfig() + .MapToConstructor(GetConstructor()) + .Ignore(e => e.Id); + + var source = new A707 { Text = "test" }; + var dest = new B707(123, "Hello"); + + var docKind = source.Adapt(config); + var mapTotarget = source.Adapt(dest,config); + + docKind.Id.ShouldBe(0); + mapTotarget.Id.ShouldBe(123); + mapTotarget.Text.ShouldBe("test"); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/723 + /// + [TestMethod] + public void MappingToIntefaceWithIgnorePrivateSetProperty() + { + TypeAdapterConfig + .NewConfig() + .TwoWays() + .Ignore(dest => dest.Ignore); + + InterfaceDestination723 dataDestination = new Data723() { Inter = "IterDataDestination", Ignore = "IgnoreDataDestination" }; + + Should.NotThrow(() => + { + var isourse = dataDestination.Adapt(); + var idestination = dataDestination.Adapt(); + }); + + } + + #region TestClasses + + public interface InterfaceDestination723 + { + public string Inter { get; set; } + public string Ignore { get; } + } + + public interface InterfaceSource723 + { + public string Inter { get; set; } + } + + private class Data723 : InterfaceSource723, InterfaceDestination723 + { + public string Ignore { get; set; } + + public string Inter { get; set; } + } + + static ConstructorInfo? GetConstructor() + { + var parameterlessCtorInfo = typeof(TDestination).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, new Type[0]); + + var ctors = typeof(TDestination).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var validCandidateCtors = ctors.Except(new[] { parameterlessCtorInfo }).ToArray(); + var ctorToUse = validCandidateCtors.Length == 1 + ? validCandidateCtors.First() + : validCandidateCtors.OrderByDescending(c => c.GetParameters().Length).First(); + + return ctorToUse; + } + public class A707 + { + public string? Text { get; set; } + } + + public class B707 + { + public int Id { get; private set; } + public string Text { get; private set; } + + public B707(int id, string text) + { + Id = id; + Text = text; + } + } + public class Poco { public Guid Id { get; set; } @@ -67,5 +172,7 @@ public class Dto [JsonIgnore] public string Name { get; set; } } + + #endregion TestClasses } } diff --git a/src/Mapster.Tests/WhenIgnoringConditionally.cs b/src/Mapster.Tests/WhenIgnoringConditionally.cs index 471760b8..9014d133 100644 --- a/src/Mapster.Tests/WhenIgnoringConditionally.cs +++ b/src/Mapster.Tests/WhenIgnoringConditionally.cs @@ -164,6 +164,8 @@ public void IgnoreIf_Apply_To_RecordType() .Compile(); var poco = new SimplePoco { Id = 1, Name = "TestName" }; + + var srt = poco.BuildAdapter().CreateMapToTargetExpression(); var dto = TypeAdapter.Adapt(poco); dto.Id.ShouldBe(1); @@ -190,8 +192,8 @@ public class SimpleDto [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class SimpleRecord { - public int Id { get; } - public string Name { get; } + public int Id { get; private set; } + public string Name { get; private set; } public SimpleRecord(int id, string name) { diff --git a/src/Mapster.Tests/WhenIgnoringNonMapped.cs b/src/Mapster.Tests/WhenIgnoringNonMapped.cs index 59a082ca..320feff1 100644 --- a/src/Mapster.Tests/WhenIgnoringNonMapped.cs +++ b/src/Mapster.Tests/WhenIgnoringNonMapped.cs @@ -12,6 +12,7 @@ public void Should_Ignore_Non_Mapped() { TypeAdapterConfig.NewConfig() .Map(dest => dest.Id, src => src.Id) + .RequireDestinationMemberSource(true) .IgnoreNonMapped(true) .Compile(); diff --git a/src/Mapster.Tests/WhenMappingDerived.cs b/src/Mapster.Tests/WhenMappingDerived.cs index 01bfac41..e6399389 100644 --- a/src/Mapster.Tests/WhenMappingDerived.cs +++ b/src/Mapster.Tests/WhenMappingDerived.cs @@ -44,6 +44,82 @@ public void WhenMappingDerivedWithoutMembers() Assert.AreEqual(inputEntity.Id, result.Id); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/794 + /// + [TestMethod] + public void WhenMapToTargetDerivedWithNullRegression() + { + var config = new TypeAdapterConfig(); + + config + .NewConfig() + .Map(dest => dest.Nested, src => src.NestedDTO) + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + config + .NewConfig() + .Map(dest => dest.SomeBaseProperty, src => src.SomeBasePropertyDTO) + .Include() + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + + config + .NewConfig() + .Map(dest => dest.SomeDerivedProperty, src => src.SomeDerivedPropertyDTO) + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + config + .NewConfig() + .MapWith(src => src.Adapt()); + + + var container = new Container794(); + var containerDTO = new ContainerDTO794(); + + container.Nested = null; + containerDTO.NestedDTO = new DerivedDTO794(); + + containerDTO.Adapt(container, config); + + (container.Nested is Derived794E).ShouldBeTrue(); // is not Base794 type, MapWith is working when Polymorphic mapping to null + } + + internal class Derived794E : Derived794 + { + + } + + internal class Base794 + { + public string SomeBaseProperty { get; set; } + } + + internal class BaseDTO794 + { + public string SomeBasePropertyDTO { get; set; } + } + + internal class Derived794 : Base794 + { + public string SomeDerivedProperty { get; set; } + } + + internal class DerivedDTO794 : BaseDTO794 + { + public string SomeDerivedPropertyDTO { get; set; } + } + + internal class Container794 + { + public Base794 Nested { get; set; } + } + + internal class ContainerDTO794 + { + public BaseDTO794 NestedDTO { get; set; } + } + internal class BaseDto { public long Id { get; set; } diff --git a/src/Mapster.Tests/WhenMappingInitProperty.cs b/src/Mapster.Tests/WhenMappingInitProperty.cs index 4fda1295..6c1352de 100644 --- a/src/Mapster.Tests/WhenMappingInitProperty.cs +++ b/src/Mapster.Tests/WhenMappingInitProperty.cs @@ -19,7 +19,7 @@ public void WhenMappingToHiddenandNewInitFieldDestination() var c = source.Adapt(); var s = source.Adapt(new BDestination()); - ((ADestination)c).Id.ShouldBe(156); + ((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping s.Id.ShouldBe(156); } @@ -33,7 +33,7 @@ public void WhenMappingToHiddenandNewInitFieldWithConstructUsing() var c = source.Adapt(); var s = source.Adapt(new BDestination()); - ((ADestination)c).Id.ShouldBe(256); + ((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping s.Id.ShouldBe(256); } diff --git a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs index 6e4bb32e..8d0fec3f 100644 --- a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs +++ b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs @@ -1,6 +1,6 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; namespace Mapster.Tests { @@ -134,8 +134,49 @@ public void Can_Map_From_Non_Nullable_Source_To_Nullable_Target() poco.IsImport.GetValueOrDefault().ShouldBeTrue(); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/414 + /// + [TestMethod] + public void MappingNullTuple() + { + TypeAdapterConfig<(string?, string?, Application414), Output414>.NewConfig() + .Map(dest => dest, src => src.Item1) + .Map(dest => dest, src => src.Item2) + .Map(dest => dest.Application, src => src.Item3 == null ? (Application414)null : new Application414() + { + Id = src.Item3.Id, + Name = src.Item3.Name + }); + + (string, string, Application414) source = (null, null, null); + + var result = source.Adapt(); + + result.Item1.ShouldBeNull(); + result.Item2.ShouldBeNull(); + result.Application.ShouldBeNull(); + } + #region TestClasses + + public class Output414 + { + public string Item1 { get; set; } + + public string Item2 { get; set; } + + public Application414 Application { get; set; } + } + + public class Application414 + { + public string Name { get; set; } + + public int Id { get; set; } + } + public class NullablePrimitivesPoco { public Guid Id { get; set; } diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index e5d719e4..9ba98adb 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -2,6 +2,7 @@ using Shouldly; using System; using System.Collections.Generic; +using static Mapster.Tests.WhenMappingDerived; namespace Mapster.Tests { @@ -14,11 +15,20 @@ public class WhenMappingRecordRegression [TestMethod] public void AdaptRecordToRecord() { + TypeAdapterConfig + .NewConfig() + .Ignore(dest => dest.Y); + var _source = new TestRecord() { X = 700 }; - var _destination = new TestRecord() { X = 500 }; + var _destination = new TestRecordY() { X = 500 , Y = 200 }; + + var _destination2 = new TestRecordY() { X = 300, Y = 400 }; var _result = _source.Adapt(_destination); + var result2 = _destination.Adapt(_destination2); + _result.X.ShouldBe(700); + _result.Y.ShouldBe(200); object.ReferenceEquals(_result, _destination).ShouldBeFalse(); } @@ -353,7 +363,104 @@ public void MappingInterfaceToInterface() } + /// + /// https://github.com/MapsterMapper/Mapster/issues/456 + /// + [TestMethod] + public void WhenRecordReceivedIgnoreCtorParamProcessing() + { + TypeAdapterConfig.NewConfig() + .Ignore(dest => dest.Name); + + TypeAdapterConfig.NewConfig() + .Ignore(dest => dest.User); + + var userDto = new UserDto456("Amichai"); + var user = new UserRecord456("John"); + var DtoInsider = new DtoInside(userDto); + var UserInsider = new UserInside(user, new UserRecord456("Skot")); + + var map = userDto.Adapt(); + var maptoTarget = userDto.Adapt(user); + + var MapToTargetInsider = DtoInsider.Adapt(UserInsider); + + map.Name.ShouldBeNullOrEmpty(); // Ignore is work set default value + maptoTarget.Name.ShouldBe("John"); // Ignore is work ignored member save value from Destination + MapToTargetInsider.User.Name.ShouldBe("John"); // Ignore is work member save value from Destination + MapToTargetInsider.SecondName.Name.ShouldBe("Skot"); // Unmached member save value from Destination + + } + + [TestMethod] + public void WhenRecordTypeWorksWithUseDestinationValueAndIgnoreNullValues() + { + + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true); + + var _source = new SourceFromTestUseDestValue() { X = 300, Y = 200, Name = new StudentNameRecord() { Name = "John" } }; + var result = _source.Adapt(); + + var _sourceFromMapToTarget = new SourceFromTestUseDestValue() { A = 100, X = null, Y = null, Name = null }; + + var txt1 = _sourceFromMapToTarget.BuildAdapter().CreateMapExpression(); + + var txt = _sourceFromMapToTarget.BuildAdapter().CreateMapToTargetExpression(); + + var _resultMapToTarget = _sourceFromMapToTarget.Adapt(result); + + result.A.ShouldBe(0); // default Value - not match + result.S.ShouldBe("Inside Data"); // is not AutoProperty not mod by source + result.Y.ShouldBe(200); // Y is AutoProperty value transmitted from source + result.Name.Name.ShouldBe("John"); // transmitted from source standart method + + _resultMapToTarget.A.ShouldBe(100); + _resultMapToTarget.X.ShouldBe(300); // Ignore NullValues work + _resultMapToTarget.Y.ShouldBe(200); // Ignore NullValues work + _resultMapToTarget.Name.Name.ShouldBe("John"); // Ignore NullValues work + + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/771 + /// https://github.com/MapsterMapper/Mapster/issues/746 + /// + [TestMethod] + public void FixCtorParamMapping() + { + var sourceRequestPaymentDto = new PaymentDTO771("MasterCard", "1234", "12/99", "234", 12); + var sourceRequestOrderDto = new OrderDTO771(Guid.NewGuid(), Guid.NewGuid(), "order123", sourceRequestPaymentDto); + var db = new Database746(UserID: "256", Password: "123"); + + + var result = new CreateOrderRequest771(sourceRequestOrderDto).Adapt(); + var resultID = db.Adapt(new Database746()); + + + result.Order.Payment.CVV.ShouldBe("234"); + resultID.UserID.ShouldBe("256"); + } + + [TestMethod] + public void RequiredProperty() + { + var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; + var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; + + TypeAdapterConfig.NewConfig() + //.Map(dest => dest.ID, source => 0) + .Ignore(x => x.ID); + var s = source.BuildAdapter().CreateMapToTargetExpression(); + + var result = source.Adapt(destination); + + result.ID.ShouldBe(245); + result.FirstMidName.ShouldBe(source.FirstMidName); + result.LastName.ShouldBe(source.LastName); + } #region NowNotWorking @@ -382,6 +489,93 @@ public void CollectionUpdate() #region TestClasses + public sealed record Database746( + string Server = "", + string Name = "", + string? UserID = null, + string? Password = null); + + public record CreateOrderRequest771(OrderDTO771 Order); + + public record CreateOrderCommand771(OrderDTO771 Order); + + + public record OrderDTO771 + ( + Guid Id, + Guid CustomerId, + string OrderName, + PaymentDTO771 Payment + ); + + public record PaymentDTO771 + ( + string CardName, + string CardNumber, + string Expiration, + string CVV, + int PaymentMethod + ); + + public class Person553 + { + + public string LastName { get; set; } + public string FirstMidName { get; set; } + } + + public class Person554 + { + public required int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + } + + + public class SourceFromTestUseDestValue + { + public int? A { get; set; } + public int? X { get; set; } + public int? Y { get; set; } + public StudentNameRecord Name { get; set; } + } + + + public record TestRecordUseDestValue() + { + private string _s = "Inside Data"; + + public int A { get; set; } + public int X { get; set; } + + [UseDestinationValue] + public int Y { get; } + + [UseDestinationValue] + public string S { get => _s; } + + [UseDestinationValue] + public StudentNameRecord Name { get; } = new StudentNameRecord() { Name = "Marta" }; + } + + public record StudentNameRecord + { + public string Name { get; set; } + } + + public record TestRecordY() + { + public int X { get; set; } + public int Y { get; set; } + } + + public record UserInside(UserRecord456 User, UserRecord456 SecondName); + public record DtoInside(UserDto456 User); + + public record UserRecord456(string Name); + + public record UserDto456(string Name); + public interface IActivityDataExtentions : IActivityData { public int TempLength { get; set; } @@ -674,14 +868,5 @@ sealed record TestSealedRecord() sealed record TestSealedRecordPositional(int X); - - - - - - - - - #endregion TestClasses } diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index bd8f28fe..88cdc66e 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -1,10 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Mapster.Tests { @@ -36,6 +31,60 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() var cCopy = c.Adapt(config); } + [TestMethod] + public void MapOpenGenericsUseInherits() + { + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Map("value", "Value"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>)) + .Map("derivedValue", "DerivedValue") + .Inherits(typeof(GenericPoco<>), typeof(GenericDto<>)); + + var poco = new DerivedPoco { Value = 123 , DerivedValue = 42 }; + var dto = poco.Adapt>(); + dto.value.ShouldBe(poco.Value); + dto.derivedValue.ShouldBe(poco.DerivedValue); + } + + [TestMethod] + public void MapOpenGenericsUseInclude() + { + TypeAdapterConfig.GlobalSettings.Clear(); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>)) + .Map("derivedValue", "DerivedValue"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Map("value", "Value"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Include(typeof(DerivedPoco<>), typeof(DerivedDto<>)); + + var poco = new DerivedPoco { Value = 123, DerivedValue = 42 }; + var dto = poco.Adapt(typeof(GenericPoco<>), typeof(GenericDto<>)); + + dto.ShouldBeOfType>(); + + ((DerivedDto)dto).value.ShouldBe(poco.Value); + ((DerivedDto)dto).derivedValue.ShouldBe(poco.DerivedValue); + } + + public class DerivedPoco : GenericPoco + { + public T DerivedValue { get; set; } + } + + public class DerivedDto : GenericDto + { + public T derivedValue { get; set; } + } + public class GenericPoco { public T Value { get; set; } diff --git a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs new file mode 100644 index 00000000..4200ada6 --- /dev/null +++ b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs @@ -0,0 +1,106 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests; + +[TestClass] +public class WhenUseDestinatonValueMappingRegression +{ + [TestClass] + public class WhenUseDestinatonMappingRegression + { + [TestMethod] + public void UseDestinatonValueUsingMapWithasParam() + { + TypeAdapterConfig> + .NewConfig() + .MapWith(src => MapThumbnailDetailsData(src).ToList()); + + var channelSrc = new ChannelSource + { + ChannelId = "123", + Thumbnails = new ThumbnailDetailsSource + { + Default = new ThumbnailSource + { + Url = "https://www.youtube.com/default.jpg" + }, + Medium = new ThumbnailSource + { + Url = "https://www.youtube.com/medium.jpg" + }, + High = new ThumbnailSource + { + Url = "https://www.youtube.com/high.jpg" + } + }, + + TempThumbnails = new List() { 1, 2, 3 } + }; + + var channelDest = channelSrc.Adapt(); + + channelDest.Thumbnails.Count.ShouldBe(3); + channelDest.TempThumbnails.Count.ShouldBe(3); + } + + + #region TestClasses + private static IEnumerable MapThumbnailDetailsData(ThumbnailDetailsSource thumbnailDetails) + { + yield return MapThumbnail(thumbnailDetails.Default, "Default"); + yield return MapThumbnail(thumbnailDetails.Medium, "Medium"); + yield return MapThumbnail(thumbnailDetails.High, "High"); + } + + private static ThumbnailDestination MapThumbnail( + ThumbnailSource thumbnail, + string thumbnailType) => + new() + { + Type = thumbnailType, + Url = thumbnail.Url.Trim(), + }; + + + public class ChannelDestination + { + public string ChannelId { get; set; } = default!; + + [UseDestinationValue] + public ICollection Thumbnails { get; } = new List(); + + [UseDestinationValue] + public ICollection TempThumbnails { get; } = new List(); + } + + public class ThumbnailDestination + { + public string Type { get; set; } = default!; + public string Url { get; set; } = default!; + } + + public class ChannelSource + { + public string ChannelId { get; set; } = default!; + public ThumbnailDetailsSource Thumbnails { get; set; } = default!; + public ICollection TempThumbnails { get; set; } = new List(); + } + + public class ThumbnailDetailsSource + { + public ThumbnailSource? Default { get; set; } + public ThumbnailSource? Medium { get; set; } + public ThumbnailSource? High { get; set; } + } + + public class ThumbnailSource + { + public string Url { get; set; } = default!; + } + + #endregion TestClasses + } +} diff --git a/src/Mapster.Tests/WhenUsingDestinationValue.cs b/src/Mapster.Tests/WhenUsingDestinationValue.cs index 086bf938..f3f7d3a8 100644 --- a/src/Mapster.Tests/WhenUsingDestinationValue.cs +++ b/src/Mapster.Tests/WhenUsingDestinationValue.cs @@ -35,6 +35,32 @@ public void MapUsingDestinationValue() poco.Strings.ShouldBe(strings); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/410 + /// + [TestMethod] + public void MappingToReadonlyPropertyWhenPocoDetectRegression() + { + var studentDto = new StudentDtoOrigin { Name = "Marta" }; + var student = studentDto.Adapt(); // No exception. + + student.Name.ShouldBe("John"); + } + + + public class StudentOrigin + { + [UseDestinationValue] + public string Name { get; } = "John"; // only readonly + } + + public class StudentDtoOrigin + { + + public string Name { get; set; } + } + + public class ContractingParty { public string Name { get; set; } diff --git a/src/Mapster/Adapters/ArrayAdapter.cs b/src/Mapster/Adapters/ArrayAdapter.cs index b2f4eaed..37f4c5e6 100644 --- a/src/Mapster/Adapters/ArrayAdapter.cs +++ b/src/Mapster/Adapters/ArrayAdapter.cs @@ -70,7 +70,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return CreateArraySet(source, destination, arg); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { if (arg.DestinationType.GetTypeInfo().IsAssignableFrom(source.Type.GetTypeInfo())) return source; diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 366f1eeb..bda195ee 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -174,39 +174,50 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de // return adapt(drvdSource); foreach (var tuple in arg.Settings.Includes) { + TypeTuple itemTuple = tuple; + + if (tuple.Source.IsOpenGenericType() && tuple.Destination.IsOpenGenericType()) + { + var genericArg = source.Type.GetGenericArguments(); + itemTuple = new TypeTuple(tuple.Source.MakeGenericType(genericArg), tuple.Destination.MakeGenericType(genericArg)); + } + //same type, no redirect to prevent endless loop - if (tuple.Source == arg.SourceType) + if (itemTuple.Source == arg.SourceType) continue; //type is not compatible, no redirect - if (!arg.SourceType.GetTypeInfo().IsAssignableFrom(tuple.Source.GetTypeInfo())) + if (!arg.SourceType.GetTypeInfo().IsAssignableFrom(itemTuple.Source.GetTypeInfo())) continue; - var drvdSource = Expression.Variable(tuple.Source); + var drvdSource = Expression.Variable(itemTuple.Source); vars.Add(drvdSource); var drvdSourceAssign = Expression.Assign( drvdSource, - Expression.TypeAs(source, tuple.Source)); + Expression.TypeAs(source, itemTuple.Source)); blocks.Add(drvdSourceAssign); - var cond = Expression.NotEqual(drvdSource, Expression.Constant(null, tuple.Source)); + var cond = Expression.NotEqual(drvdSource, Expression.Constant(null, itemTuple.Source)); ParameterExpression? drvdDest = null; if (destination != null) { - drvdDest = Expression.Variable(tuple.Destination); + drvdDest = Expression.Variable(itemTuple.Destination); vars.Add(drvdDest); var drvdDestAssign = Expression.Assign( drvdDest, - Expression.TypeAs(destination, tuple.Destination)); + Expression.TypeAs(destination, itemTuple.Destination)); blocks.Add(drvdDestAssign); - cond = Expression.AndAlso( - cond, - Expression.NotEqual(drvdDest, Expression.Constant(null, tuple.Destination))); + + // fix by https://github.com/MapsterMapper/Mapster/issues/794 + // This can be removed if it does not cause any other bugs. + // cond = Expression.AndAlso( + // cond, + // Expression.NotEqual(drvdDest, Expression.Constant(null, tuple.Destination))); } - var adaptExpr = CreateAdaptExpressionCore(drvdSource, tuple.Destination, arg, destination: drvdDest); + var adaptExpr = CreateAdaptExpressionCore(drvdSource, itemTuple.Destination, arg, destination: drvdDest); var adapt = Expression.Return(label, adaptExpr); var ifExpr = Expression.IfThen(cond, adapt); blocks.Add(ifExpr); @@ -221,7 +232,17 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de vars.Add(src); transformedSource = src; } - var set = CreateInstantiationExpression(transformedSource, destination, arg); + + Expression? set; + var requiremembers = arg.DestinationType.GetProperties() + .Where(x => x.GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))); + + if (requiremembers.Count() != 0) + set = CreateInlineExpression(source, arg, true); + else + set = CreateInstantiationExpression(transformedSource, destination, arg); + if (destination != null && (UseTargetValue || arg.UseDestinationValue) && arg.GetConstructUsing()?.Parameters.Count != 2) { if (destination.CanBeNull()) @@ -388,7 +409,8 @@ private static Expression InvokeMapping( } protected abstract Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg); - protected abstract Expression? CreateInlineExpression(Expression source, CompileArgument arg); + protected abstract Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false); + protected Expression CreateInstantiationExpression(Expression source, CompileArgument arg) { @@ -495,6 +517,21 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp if (transform != null) exp = transform.TransformFunc(exp.Type).Apply(arg.MapType, exp); } + else + { + if (exp.NodeType != ExpressionType.Invoke) + { + var argExt = new CompileArgument + { + DestinationType = arg.DestinationType, + SourceType = arg.DestinationType, + MapType = MapType.MapToTarget, + Context = arg.Context, + }; + + return CreateAdaptExpressionCore(exp, destinationType, argExt, mapping, destination).To(destinationType); + } + } return exp.To(destinationType); } diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index faa490ec..d419932f 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -1,10 +1,10 @@ -using System; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Models; -using Mapster.Utils; namespace Mapster.Adapters { @@ -15,12 +15,16 @@ internal abstract class BaseClassAdapter : BaseAdapter #region Build the Adapter Model - protected ClassMapping CreateClassConverter(Expression source, ClassModel classModel, CompileArgument arg, Expression? destination = null) + protected ClassMapping CreateClassConverter(Expression source, ClassModel classModel, CompileArgument arg, Expression? destination = null, bool ctorMapping = false, ClassModel recordRestorMemberModel = null) { var destinationMembers = classModel.Members; var unmappedDestinationMembers = new List(); var properties = new List(); + arg.ConstructorMapping = ctorMapping; + if (arg.Settings.IgnoreNonMapped == true) + IgnoreNonMapped(classModel,arg); + var sources = new List {source}; sources.AddRange( arg.Settings.ExtraSources.Select(src => @@ -29,7 +33,7 @@ src is LambdaExpression lambda : ExpressionEx.PropertyOrFieldPath(source, (string)src))); foreach (var destinationMember in destinationMembers) { - if (ProcessIgnores(arg, destinationMember, out var ignore)) + if (ProcessIgnores(arg, destinationMember, out var ignore) && !ctorMapping) continue; var resolvers = arg.Settings.ValueAccessingStrategies.AsEnumerable(); @@ -40,6 +44,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(); @@ -54,6 +115,20 @@ select fn(src, destinationMember, arg)) Destination = (ParameterExpression?)destination, UseDestinationValue = arg.MapType != MapType.Projection && destinationMember.UseDestinationValue(arg), }; + if(getter == null && !arg.DestinationType.IsRecordType() + && destinationMember.Info is PropertyInfo propinfo) + { + if (propinfo.GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))) + { + getter = destinationMember.Type.CreateDefault(); + } + } + + if (arg.MapType == MapType.MapToTarget && getter == null && arg.DestinationType.IsRecordType()) + { + getter = TryRestoreRecordMember(destinationMember, recordRestorMemberModel, destination) ?? getter; + } if (getter != null) { propertyModel.Getter = arg.MapType == MapType.Projection @@ -80,7 +155,8 @@ select fn(src, destinationMember, arg)) { if (classModel.BreakOnUnmatched) return null!; - unmappedDestinationMembers.Add(destinationMember.Name); + if(!arg.Settings.Ignore.Any(x=>x.Key == destinationMember.Name)) // Don't mark a constructor parameter if it was explicitly ignored + unmappedDestinationMembers.Add(destinationMember.Name); } properties.Add(propertyModel); @@ -128,7 +204,7 @@ protected static bool ProcessIgnores( && ignore.Condition == null; } - protected Expression CreateInstantiationExpression(Expression source, ClassMapping classConverter, CompileArgument arg) + protected Expression CreateInstantiationExpression(Expression source, ClassMapping classConverter, CompileArgument arg, Expression? destination, ClassModel recordRestorParamModel = null) { var members = classConverter.Members; @@ -144,6 +220,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi if (member.Getter == null) { getter = defaultConst; + + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + getter = TryRestoreRecordMember(member.DestinationMember,recordRestorParamModel,destination) ?? getter; } else { @@ -156,6 +235,14 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var condition = ExpressionEx.Not(body); getter = Expression.Condition(condition, getter, defaultConst); } + else + if (arg.Settings.Ignore.Any(x => x.Key == member.DestinationMember.Name)) + { + getter = defaultConst; + + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + getter = TryRestoreRecordMember(member.DestinationMember, recordRestorParamModel, destination) ?? getter; + } } arguments.Add(getter); } @@ -181,6 +268,47 @@ protected virtual ClassModel GetSetterModel(CompileArgument arg) }; } + protected void IgnoreNonMapped (ClassModel classModel, CompileArgument arg) + { + var notMappingToIgnore = classModel.Members + .ExceptBy(arg.Settings.Resolvers.Select(x => x.DestinationMemberName), + y => y.Name); + + foreach (var item in notMappingToIgnore) + { + arg.Settings.Ignore.TryAdd(item.Name, new IgnoreDictionary.IgnoreItem()); + } + } + + protected virtual ClassModel GetOnlyRequiredPropertySetterModel(CompileArgument arg) + { + return new ClassModel + { + Members = arg.DestinationType.GetFieldsAndProperties(true) + .Where(x => x.GetType() == typeof(PropertyModel)) + .Where(y => ((PropertyInfo)y.Info).GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))) + }; + } + + protected Expression? TryRestoreRecordMember(IMemberModelEx member, ClassModel? restorRecordModel, Expression? destination) + { + if (restorRecordModel != null && destination != null) + { + var find = restorRecordModel.Members + .Where(x => x.Name == member.Name).FirstOrDefault(); + + if (find != null) + { + var compareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + return Expression.Condition(compareNull, member.Type.CreateDefault(), Expression.MakeMemberAccess(destination, (MemberInfo)find.Info)); + } + + } + + return null; + } + #endregion } } diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 407c5a5b..5e20f0a2 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -69,19 +69,19 @@ protected override Expression CreateInstantiationExpression(Expression source, E classConverter = destType.GetConstructors() .OrderByDescending(it => it.GetParameters().Length) .Select(it => GetConstructorModel(it, true)) - .Select(it => CreateClassConverter(source, it, arg)) + .Select(it => CreateClassConverter(source, it, arg, ctorMapping:true)) .FirstOrDefault(it => it != null); } else { var model = GetConstructorModel(ctor, false); - classConverter = CreateClassConverter(source, model, arg); + classConverter = CreateClassConverter(source, model, arg, ctorMapping:true); } if (classConverter == null) return base.CreateInstantiationExpression(source, destination, arg); - return CreateInstantiationExpression(source, classConverter, arg); + return CreateInstantiationExpression(source, classConverter, arg, destination); } protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) @@ -191,7 +191,7 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre new[] { member.Destination, memberAsObject }); } - protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { //new TDestination { // Prop1 = convert(src.Prop1), @@ -202,8 +202,18 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre var memberInit = exp as MemberInitExpression; var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); - var classModel = GetSetterModel(arg); - var classConverter = CreateClassConverter(source, classModel, arg); + ClassModel? classModel; + ClassMapping? classConverter; + if (IsRequiredOnly) + { + classModel = GetOnlyRequiredPropertySetterModel(arg); + classConverter = CreateClassConverter(source, classModel, arg, ctorMapping: true); + } + else + { + classModel = GetSetterModel(arg); + classConverter = CreateClassConverter(source, classModel, arg); + } var members = classConverter.Members; var lines = new List(); diff --git a/src/Mapster/Adapters/CollectionAdapter.cs b/src/Mapster/Adapters/CollectionAdapter.cs index 861b1383..01189614 100644 --- a/src/Mapster/Adapters/CollectionAdapter.cs +++ b/src/Mapster/Adapters/CollectionAdapter.cs @@ -115,7 +115,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Block(actions); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { if (arg.DestinationType.GetTypeInfo().IsAssignableFrom(source.Type.GetTypeInfo()) && (arg.Settings.ShallowCopyForSameType == true || arg.MapType == MapType.Projection)) diff --git a/src/Mapster/Adapters/DelegateAdapter.cs b/src/Mapster/Adapters/DelegateAdapter.cs index 0c16e315..2eb33ab3 100644 --- a/src/Mapster/Adapters/DelegateAdapter.cs +++ b/src/Mapster/Adapters/DelegateAdapter.cs @@ -30,7 +30,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster/Adapters/DictionaryAdapter.cs b/src/Mapster/Adapters/DictionaryAdapter.cs index 25af8e3b..8d93aabd 100644 --- a/src/Mapster/Adapters/DictionaryAdapter.cs +++ b/src/Mapster/Adapters/DictionaryAdapter.cs @@ -181,7 +181,7 @@ private Expression CreateSetFromKvp(Expression kvp, Expression key, Expression d return destSetFn(destination, key, value); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { //new TDestination { // { "Prop1", convert(src.Prop1) }, diff --git a/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs b/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs index 01fa6931..8c44d04a 100644 --- a/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs +++ b/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs @@ -91,7 +91,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return CreateArraySet(source, destination, arg); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new NotImplementedException(); } diff --git a/src/Mapster/Adapters/ObjectAdapter.cs b/src/Mapster/Adapters/ObjectAdapter.cs index 71817886..3140549a 100644 --- a/src/Mapster/Adapters/ObjectAdapter.cs +++ b/src/Mapster/Adapters/ObjectAdapter.cs @@ -27,7 +27,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster/Adapters/PrimitiveAdapter.cs b/src/Mapster/Adapters/PrimitiveAdapter.cs index cfc7c809..c2d411db 100644 --- a/src/Mapster/Adapters/PrimitiveAdapter.cs +++ b/src/Mapster/Adapters/PrimitiveAdapter.cs @@ -18,6 +18,8 @@ protected override bool CanMap(PreCompileArgument arg) protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) { + if (arg.SourceType != arg.DestinationType && arg.Context.Config.RequireExplicitMappingPrimitive && !arg.ExplicitMapping) + throw new InvalidOperationException("Implicit mapping is not allowed (check GlobalSettings.RequireExplicitMapping) and no configuration exists"); if (arg.Settings.MapToTargetPrimitive == true) { @@ -92,7 +94,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio throw new NotImplementedException(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new NotImplementedException(); } diff --git a/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs index 3703c281..ea4aa6e1 100644 --- a/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs +++ b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs @@ -38,8 +38,8 @@ protected override Expression CreateInstantiationExpression(Expression source, E return base.CreateInstantiationExpression(source, destination, arg); var ctor = destType.GetConstructors()[0]; var classModel = GetConstructorModel(ctor, false); - var classConverter = CreateClassConverter(source, classModel, arg); - return CreateInstantiationExpression(source, classConverter, arg); + var classConverter = CreateClassConverter(source, classModel, arg, ctorMapping:true); + return CreateInstantiationExpression(source, classConverter, arg, destination); } else return base.CreateInstantiationExpression(source,destination, arg); diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 009af932..77f75f92 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -1,52 +1,60 @@ -using System.Collections.Generic; +using Mapster.Models; +using Mapster.Utils; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Utils; +using static Mapster.IgnoreDictionary; namespace Mapster.Adapters { internal class RecordTypeAdapter : ClassAdapter { + private ClassMapping? ClassConverterContext; protected override int Score => -149; protected override bool UseTargetValue => false; + private List SkipIgnoreNullValuesMemberMap = new List(); + protected override bool CanMap(PreCompileArgument arg) { return arg.DestinationType.IsRecordType(); } - protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) - { - //new TDestination(src.Prop1, src.Prop2) - - if (arg.GetConstructUsing() != null) - return base.CreateInstantiationExpression(source, destination, arg); - - var destType = arg.DestinationType.GetTypeInfo().IsInterface - ? DynamicTypeGenerator.GetTypeForInterface(arg.DestinationType, arg.Settings.Includes.Count > 0) - : arg.DestinationType; - if (destType == null) - return base.CreateInstantiationExpression(source, destination, arg); - var ctor = destType.GetConstructors() - .OrderByDescending(it => it.GetParameters().Length).ToArray().FirstOrDefault(); // Will be used public constructor with the maximum number of parameters - var classModel = GetConstructorModel(ctor, false); - var classConverter = CreateClassConverter(source, classModel, arg); - var installExpr = CreateInstantiationExpression(source, classConverter, arg); - return RecordInlineExpression(source, arg, installExpr); // Activator field when not include in public ctor - } - - protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) { - return Expression.Empty(); + return false; } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return base.CreateInstantiationExpression(source, arg); } + protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) + { + //new TDestination(src.Prop1, src.Prop2) + + SkipIgnoreNullValuesMemberMap.Clear(); + Expression installExpr; + + if (arg.GetConstructUsing() != null || arg.DestinationType == null) + installExpr = base.CreateInstantiationExpression(source, destination, arg); + else + { + var ctor = arg.DestinationType.GetConstructors() + .OrderByDescending(it => it.GetParameters().Length).ToArray().FirstOrDefault(); // Will be used public constructor with the maximum number of parameters + var classModel = GetConstructorModel(ctor, false); + var restorParamModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg, ctorMapping: true); + installExpr = CreateInstantiationExpression(source, classConverter, arg, destination, restorParamModel); + } + - private Expression? RecordInlineExpression(Expression source, CompileArgument arg, Expression installExpr) + return RecordInlineExpression(source, destination, arg, installExpr); // Activator field when not include in public ctor + } + + private Expression? RecordInlineExpression(Expression source, Expression? destination, CompileArgument arg, Expression installExpr) { //new TDestination { // Prop1 = convert(src.Prop1), @@ -56,27 +64,51 @@ protected override Expression CreateInlineExpression(Expression source, CompileA var exp = installExpr; var memberInit = exp as MemberInitExpression; var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; - var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); + var contructorMembers = newInstance.Constructor?.GetParameters().ToList() ?? new(); var classModel = GetSetterModel(arg); - var classConverter = CreateClassConverter(source, classModel, arg); + var classConverter = CreateClassConverter(source, classModel, arg, destination: destination, recordRestorMemberModel: classModel); var members = classConverter.Members; + ClassConverterContext = classConverter; + var lines = new List(); if (memberInit != null) lines.AddRange(memberInit.Bindings); foreach (var member in members) { - 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)) + && contructorMembers.Any(x => string.Equals(x.Name, member.DestinationMember.Name, StringComparison.InvariantCultureIgnoreCase))) continue; if (member.DestinationMember.SetterModifier == AccessModifier.None) continue; - var value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support + { + if (arg.MapType != MapType.MapToTarget) + { + SkipIgnoreNullValuesMemberMap.Add(member); + continue; + } + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); + adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); + } + + + //special null property check for projection //if we don't set null to property, EF will create empty object @@ -87,14 +119,196 @@ protected override Expression CreateInlineExpression(Expression source, CompileA && !member.DestinationMember.Type.IsCollection() && member.Getter.Type.GetTypeInfo().GetCustomAttributesData().All(attr => attr.GetAttributeType().Name != "ComplexTypeAttribute")) { - value = member.Getter.NotNullReturn(value); + adapt = member.Getter.NotNullReturn(adapt); } - var bind = Expression.Bind((MemberInfo)member.DestinationMember.Info!, value); + var bind = Expression.Bind((MemberInfo)member.DestinationMember.Info!, adapt); lines.Add(bind); } + if (arg.MapType == MapType.MapToTarget) + lines.AddRange(RecordIngnoredWithoutConditonRestore(destination, arg, contructorMembers, classModel)); + return Expression.MemberInit(newInstance, lines); } + + private List RecordIngnoredWithoutConditonRestore(Expression? destination, CompileArgument arg, List contructorMembers, ClassModel restorPropertyModel) + { + var members = restorPropertyModel.Members + .Where(x => arg.Settings.Ignore.Any(y => y.Key == x.Name)); + + var lines = new List(); + + + foreach (var member in members) + { + if (destination == null) + continue; + + IgnoreItem ignore; + ProcessIgnores(arg, member, out ignore); + + if (member.SetterModifier == AccessModifier.None || + ignore.Condition != null || + contructorMembers.Any(x => string.Equals(x.Name, member.Name, StringComparison.InvariantCultureIgnoreCase))) + continue; + + lines.Add(Expression.Bind((MemberInfo)member.Info, Expression.MakeMemberAccess(destination, (MemberInfo)member.Info))); + } + + return lines; + } + + protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) + { + // Mapping property Without setter when UseDestinationValue == true + + var result = destination; + var classModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg, result); + var members = classConverter.Members; + + var lines = new List(); + + if (arg.MapType != MapType.MapToTarget) + { + foreach (var member in SkipIgnoreNullValuesMemberMap) + { + + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + adapt = member.DestinationMember.SetExpression(destination, adapt); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + + + lines.Add(Expression.IfThen(sourceCondition, adapt)); + } + } + + + foreach (var member in members) + { + if (member.DestinationMember.SetterModifier == AccessModifier.None && member.UseDestinationValue) + { + + if (member.DestinationMember is PropertyModel && member.DestinationMember.Type.IsValueType + || member.DestinationMember.Type.IsMapsterPrimitive() + || member.DestinationMember.Type.IsRecordType()) + { + + Expression adapt; + if (member.DestinationMember.Type.IsRecordType()) + adapt = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, member.Getter); + else + adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, result); + + var blocks = Expression.Block(SetValueTypeAutoPropertyByReflection(member, adapt, classModel)); + var lambda = Expression.Lambda(blocks, parameters: new[] { (ParameterExpression)source, (ParameterExpression)destination }); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + { + + if (arg.MapType != MapType.MapToTarget) + { + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThen(condition, Expression.Invoke(lambda, source, destination))); + continue; + } + + if (arg.MapType == MapType.MapToTarget) + { + var var2Param = ClassConverterContext.Members.Where(x => x.DestinationMember.Name == member.DestinationMember.Name).FirstOrDefault(); + + Expression destMemberVar2 = var2Param.DestinationMember.GetExpression(var2Param.Destination); + var ParamLambdaVar2 = destMemberVar2; + if(member.DestinationMember.Type.IsRecordType()) + ParamLambdaVar2 = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, destMemberVar2); + + var blocksVar2 = Expression.Block(SetValueTypeAutoPropertyByReflection(member, ParamLambdaVar2, classModel)); + var lambdaVar2 = Expression.Lambda(blocksVar2, parameters: new[] { (ParameterExpression)var2Param.Destination, (ParameterExpression)destination }); + var adaptVar2 = Expression.Invoke(lambdaVar2, var2Param.Destination, destination); + + + Expression conditionVar2; + if (destMemberVar2.CanBeNull()) + { + var complexcheck = Expression.AndAlso(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), // if(var2 != null && var2.Prop != null) + Expression.NotEqual(destMemberVar2, Expression.Constant(null, var2Param.Getter.Type))); + conditionVar2 = Expression.IfThen(complexcheck, adaptVar2); + } + else + conditionVar2 = Expression.IfThen(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), adaptVar2); + + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThenElse(condition, Expression.Invoke(lambda, source, destination), conditionVar2)); + continue; + } + } + + lines.Add(Expression.Invoke(lambda, source, destination)); + } + else + { + var destMember = member.DestinationMember.GetExpression(destination); + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + { + if (arg.MapType != MapType.MapToTarget) + { + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThen(condition, adapt)); + continue; + } + if (arg.MapType == MapType.MapToTarget) + { + var var2Param = ClassConverterContext.Members.Where(x => x.DestinationMember.Name == member.DestinationMember.Name).FirstOrDefault(); + + var destMemberVar2 = var2Param.DestinationMember.GetExpression(var2Param.Destination); + var adaptVar2 = CreateAdaptExpression(destMemberVar2, member.DestinationMember.Type, arg, var2Param, destMember); + + var complexcheck = Expression.AndAlso(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), // if(var2 != null && var2.Prop != null) + Expression.NotEqual(destMemberVar2, Expression.Constant(null, var2Param.Getter.Type))); + var conditionVar2 = Expression.IfThen(complexcheck, adaptVar2); + + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThenElse(condition, adapt, conditionVar2)); + continue; + } + + + } + + lines.Add(adapt); + } + + } + } + + return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); + } + + protected static Expression SetValueTypeAutoPropertyByReflection(MemberMapping member, Expression adapt, ClassModel checkmodel) + { + var modDesinationMemeberName = $"<{member.DestinationMember.Name}>k__BackingField"; + if (checkmodel.Members.Any(x => x.Name == modDesinationMemeberName) == false) // Property is not autoproperty + return Expression.Empty(); + var typeofExpression = Expression.Constant(member.Destination!.Type); + var getPropertyMethod = typeof(Type).GetMethod("GetField", new[] { typeof(string), typeof(BindingFlags) })!; + var getPropertyExpression = Expression.Call(typeofExpression, getPropertyMethod, + Expression.Constant(modDesinationMemeberName), Expression.Constant(BindingFlags.Instance | BindingFlags.NonPublic)); + var setValueMethod = + typeof(FieldInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) })!; + var memberAsObject = adapt.To(typeof(object)); + return Expression.Call(getPropertyExpression, setValueMethod, + new[] { member.Destination, memberAsObject }); + } } } diff --git a/src/Mapster/Compile/CompileArgument.cs b/src/Mapster/Compile/CompileArgument.cs index 2e15637a..23c540ea 100644 --- a/src/Mapster/Compile/CompileArgument.cs +++ b/src/Mapster/Compile/CompileArgument.cs @@ -15,6 +15,7 @@ public class CompileArgument public TypeAdapterSettings Settings { get; set; } public CompileContext Context { get; set; } public bool UseDestinationValue { get; set; } + public bool? ConstructorMapping { get; set; } private HashSet? _srcNames; internal HashSet GetSourceNames() 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/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index ea47d9ad..fd13407d 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -73,10 +73,10 @@ public static class ValueAccessingStrategy { var members = source.Type.GetFieldsAndProperties(true); var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); return members .Where(member => member.ShouldMapMember(arg, MemberSide.Source)) - .Where(member => member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter) == destinationMemberName) + .Where(member => member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg) == destinationMemberName) .Select(member => member.GetExpression(source)) .FirstOrDefault(); } @@ -86,7 +86,7 @@ public static class ValueAccessingStrategy if (arg.MapType == MapType.Projection) return null; var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = "Get" + destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = "Get" + destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var getMethod = Array.Find(source.Type.GetMethods(BindingFlags.Public | BindingFlags.Instance), m => strategy.SourceMemberNameConverter(m.Name) == destinationMemberName && m.GetParameters().Length == 0); if (getMethod == null) return null; @@ -98,7 +98,7 @@ public static class ValueAccessingStrategy private static Expression? FlattenMemberFn(Expression source, IMemberModel destinationMember, CompileArgument arg) { var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); return GetDeepFlattening(source, destinationMemberName, arg); } @@ -111,7 +111,7 @@ public static class ValueAccessingStrategy if (!member.ShouldMapMember(arg, MemberSide.Source)) continue; - var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter); + var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg); if (string.Equals(propertyName, sourceMemberName)) return member.GetExpression(source); @@ -132,14 +132,14 @@ public static class ValueAccessingStrategy internal static IEnumerable FindUnflatteningPairs(Expression source, IMemberModel destinationMember, CompileArgument arg) { var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var members = source.Type.GetFieldsAndProperties(true); foreach (var member in members) { if (!member.ShouldMapMember(arg, MemberSide.Source)) continue; - var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter); + var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg); if (!sourceMemberName.StartsWith(destinationMemberName) || sourceMemberName == destinationMemberName) continue; foreach (var prop in GetDeepUnflattening(destinationMember, sourceMemberName.Substring(destinationMemberName.Length).TrimStart('_'), arg)) @@ -161,7 +161,7 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM { if (!member.ShouldMapMember(arg, MemberSide.Destination)) continue; - var destMemberName = member.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destMemberName = member.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var propertyType = member.Type; if (string.Equals(propertyName, destMemberName)) { @@ -185,7 +185,7 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM return null; var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var key = Expression.Constant(destinationMemberName); var args = dictType.GetGenericArguments(); if (strategy.SourceMemberNameConverter != MapsterHelper.Identity) diff --git a/src/Mapster/TypeAdapter.cs b/src/Mapster/TypeAdapter.cs index 0ac37924..f30f55fa 100644 --- a/src/Mapster/TypeAdapter.cs +++ b/src/Mapster/TypeAdapter.cs @@ -136,6 +136,12 @@ private static TDestination UpdateFuncFromPackedinObject( /// Adapted destination type. public static object? Adapt(this object source, Type sourceType, Type destinationType) { + if (source != null && + sourceType.IsOpenGenericType() && destinationType.IsOpenGenericType()) + { + var arg = source.GetType().GetGenericArguments(); + return Adapt(source, sourceType.MakeGenericType(arg), destinationType.MakeGenericType(arg), TypeAdapterConfig.GlobalSettings); + } return Adapt(source, sourceType, destinationType, TypeAdapterConfig.GlobalSettings); } diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index cb8ac881..a6633bb0 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -13,6 +13,8 @@ namespace Mapster { public class TypeAdapterConfig { + public Type SourceType { get; protected set; } + public Type DestinationType { get; protected set; } public static List RulesTemplate { get; } = CreateRuleTemplate(); public static TypeAdapterConfig GlobalSettings { get; } = new TypeAdapterConfig(); @@ -82,6 +84,7 @@ private static List CreateRuleTemplate() public bool RequireDestinationMemberSource { get; set; } public bool RequireExplicitMapping { get; set; } + public bool RequireExplicitMappingPrimitive { get; set; } public bool AllowImplicitDestinationInheritance { get; set; } public bool AllowImplicitSourceInheritance { get; set; } = true; public bool SelfContainedCodeGeneration { get; set; } @@ -147,6 +150,9 @@ public TypeAdapterSetter When(Func canMap) /// public TypeAdapterSetter NewConfig() { + this.SourceType = typeof(TSource); + this.DestinationType = typeof(TDestination); + Remove(typeof(TSource), typeof(TDestination)); return ForType(); } @@ -160,6 +166,9 @@ public TypeAdapterSetter NewConfig /// public TypeAdapterSetter NewConfig(Type sourceType, Type destinationType) { + this.SourceType = sourceType; + this.DestinationType = destinationType; + Remove(sourceType, destinationType); return ForType(sourceType, destinationType); } @@ -173,6 +182,9 @@ public TypeAdapterSetter NewConfig(Type sourceType, Type destinationType) /// public TypeAdapterSetter ForType() { + this.SourceType = typeof(TSource); + this.DestinationType = typeof(TDestination); + var key = new TypeTuple(typeof(TSource), typeof(TDestination)); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); @@ -187,6 +199,9 @@ public TypeAdapterSetter ForType() /// public TypeAdapterSetter ForType(Type sourceType, Type destinationType) { + this.SourceType = sourceType; + this.DestinationType = destinationType; + var key = new TypeTuple(sourceType, destinationType); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); @@ -200,6 +215,9 @@ public TypeAdapterSetter ForType(Type sourceType, Type destinationType) /// public TypeAdapterSetter ForDestinationType() { + this.SourceType = typeof(void); + this.DestinationType = typeof(TDestination); + var key = new TypeTuple(typeof(void), typeof(TDestination)); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); @@ -213,6 +231,9 @@ public TypeAdapterSetter ForDestinationType() /// public TypeAdapterSetter ForDestinationType(Type destinationType) { + this.SourceType = typeof(void); + this.DestinationType = destinationType; + var key = new TypeTuple(typeof(void), destinationType); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); @@ -495,7 +516,7 @@ private LambdaExpression CreateMapInvokeExpression(Type sourceType, Type destina internal Expression CreateMapInvokeExpressionBody(Type sourceType, Type destinationType, Expression p) { - if (RequireExplicitMapping) + if (RequireExplicitMapping || RequireExplicitMappingPrimitive) { var key = new TypeTuple(sourceType, destinationType); _mapDict[key] = Compiler(CreateMapExpression(key, MapType.Map)); @@ -518,7 +539,7 @@ internal Expression CreateMapInvokeExpressionBody(Type sourceType, Type destinat internal Expression CreateMapToTargetInvokeExpressionBody(Type sourceType, Type destinationType, Expression p1, Expression p2) { - if (RequireExplicitMapping) + if (RequireExplicitMapping || RequireExplicitMappingPrimitive) { var key = new TypeTuple(sourceType, destinationType); _mapToTargetDict[key] = Compiler(CreateMapExpression(key, MapType.MapToTarget)); diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index a0581845..026f7501 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -269,10 +269,30 @@ public static TSetter UseDestinationValue(this TSetter setter, Func(this TSetter setter, Type sourceType, Type destType) where TSetter : TypeAdapterSetter + public static TSetter Include(this TSetter setter, Type sourceType, Type destType) where TSetter : TypeAdapterSetter { setter.CheckCompiled(); + Type baseSourceType = setter.Config.SourceType; + Type baseDestinationType = setter.Config.DestinationType; + + if (baseSourceType.IsOpenGenericType() && baseDestinationType.IsOpenGenericType()) + { + if (!sourceType.IsAssignableToGenericType(baseSourceType)) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + if (!destType.IsAssignableToGenericType(baseDestinationType)) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + else + { + if (!baseSourceType.GetTypeInfo().IsAssignableFrom(sourceType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + + if (!baseDestinationType.GetTypeInfo().IsAssignableFrom(destType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + + setter.Config.Rules.LockAdd(new TypeAdapterRule { Priority = arg => @@ -286,6 +306,36 @@ internal static TSetter Include(this TSetter setter, Type sourceType, T return setter; } + public static TSetter Inherits(this TSetter setter, Type baseSourceType, Type baseDestinationType) where TSetter : TypeAdapterSetter + { + setter.CheckCompiled(); + + Type derivedSourceType = setter.Config.SourceType; + Type derivedDestinationType = setter.Config.DestinationType; + + if(baseSourceType.IsOpenGenericType() && baseDestinationType.IsOpenGenericType()) + { + if (!derivedSourceType.IsAssignableToGenericType(baseSourceType)) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + if (!derivedDestinationType.IsAssignableToGenericType(baseDestinationType)) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + else + { + if (!baseSourceType.GetTypeInfo().IsAssignableFrom(derivedSourceType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + + if (!baseDestinationType.GetTypeInfo().IsAssignableFrom(derivedDestinationType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + + if (setter.Config.RuleMap.TryGetValue(new TypeTuple(baseSourceType, baseDestinationType), out var rule)) + { + setter.Settings.Apply(rule.Settings); + } + return setter; + } + public static TSetter ApplyAdaptAttribute(this TSetter setter, BaseAdaptAttribute attr) where TSetter : TypeAdapterSetter { if (attr.IgnoreAttributes != null) @@ -812,20 +862,8 @@ public TypeAdapterSetter Inherits Fork(Action action) 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/NullableExpressionVisitor.cs b/src/Mapster/Utils/NullableExpressionVisitor.cs index ed642996..70d25162 100644 --- a/src/Mapster/Utils/NullableExpressionVisitor.cs +++ b/src/Mapster/Utils/NullableExpressionVisitor.cs @@ -127,7 +127,7 @@ protected override Expression VisitConstant(ConstantExpression node) protected override Expression VisitMember(MemberExpression node) { - CanBeNull = node.Member.GetCustomAttributesData().All(IsNullable); + CanBeNull = node.Type.IsClass || node.Member.GetCustomAttributesData().All(IsNullable); return node; } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index dae413d8..93aa7e62 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) @@ -61,6 +61,12 @@ public static bool IsPoco(this Type type) if (type.IsConvertible()) return false; + if (type == typeof(Type) || type.BaseType == typeof(MulticastDelegate)) + return false; + + if (type.IsClass && type.GetProperties().Count() != 0) + return true; + return type.GetFieldsAndProperties().Any(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) != 0); } @@ -69,23 +75,44 @@ public static IEnumerable GetFieldsAndProperties(this Type type, var bindingFlags = BindingFlags.Instance | BindingFlags.Public; if (includeNonPublic) bindingFlags |= BindingFlags.NonPublic; - + + var currentTypeMembers = type.FindMembers(MemberTypes.Property | MemberTypes.Field, + BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + (x, y) => true, type.FullName); + if (type.GetTypeInfo().IsInterface) { var allInterfaces = GetAllInterfaces(type); - return allInterfaces.SelectMany(GetPropertiesFunc); + return allInterfaces.SelectMany(x => GetPropertiesFunc(x, currentTypeMembers)); } - return GetPropertiesFunc(type).Concat(GetFieldsFunc(type)); + return GetPropertiesFunc(type, currentTypeMembers).Concat(GetFieldsFunc(type, currentTypeMembers)); - IEnumerable GetPropertiesFunc(Type t) => t.GetProperties(bindingFlags) - .Where(x => x.GetIndexParameters().Length == 0) + IEnumerable GetPropertiesFunc(Type t, MemberInfo[] currentTypeMembers) => t.GetProperties(bindingFlags) + .Where(x => x.GetIndexParameters().Length == 0).DropHiddenMembers(currentTypeMembers) .Select(CreateModel); - IEnumerable GetFieldsFunc(Type t) => t.GetFields(bindingFlags) + IEnumerable GetFieldsFunc(Type t, MemberInfo[] overlapMembers) => + t.GetFields(bindingFlags).DropHiddenMembers(overlapMembers) .Select(CreateModel); } + public static IEnumerable DropHiddenMembers(this IEnumerable allMembers, ICollection currentTypeMembers) where T : MemberInfo + { + var compareMemberNames = allMembers.IntersectBy(currentTypeMembers.Select(x => x.Name), x => x.Name).Select(x => x.Name); + + foreach (var member in allMembers) + { + if (compareMemberNames.Contains(member.Name)) + { + if (currentTypeMembers.First(x => x.Name == member.Name).MetadataToken == member.MetadataToken) + yield return member; + } + else + yield return member; + } + } + // GetProperties(), GetFields(), GetMethods() do not return properties/methods from parent interfaces, // so we need to process every one of them separately. public static IEnumerable GetAllInterfaces(this Type interfaceType) @@ -311,11 +338,14 @@ public static bool UseDestinationValue(this IMemberModel member, CompileArgument return predicates.Any(predicate => predicate(member)); } - public static string GetMemberName(this IMemberModel member, MemberSide side, List> getMemberNames, Func nameConverter) + public static string GetMemberName(this IMemberModel member, MemberSide side, List> getMemberNames, Func nameConverter, CompileArgument arg) { var memberName = getMemberNames.Select(func => func(member, side)) - .FirstOrDefault(name => name != null) - ?? member.Name; + .FirstOrDefault(name => name != null); + if (memberName == null && arg.ConstructorMapping == true) + memberName = member.Name.ToPascalCase(); + if (memberName == null) + memberName = member.Name; return nameConverter(memberName); } @@ -380,5 +410,24 @@ public static bool IsInitOnly(this PropertyInfo propertyInfo) var isExternalInitType = typeof(System.Runtime.CompilerServices.IsExternalInit); return setMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType); } + + public static bool IsAssignableToGenericType(this Type derivedType, Type genericType) + { + + if (derivedType.IsGenericType && derivedType.BaseType.GUID == genericType.GUID) + return true; + + Type baseType = derivedType.BaseType; + if (baseType == null) return false; + + return IsAssignableToGenericType(baseType, genericType); + } + public static bool IsOpenGenericType(this Type type) + { + if(type.IsGenericType) + return type.GetGenericArguments().All(x=>x.GUID == Guid.Empty); + + return false; + } } } \ No newline at end of file