From 653679ad324cf002c6201fb41e375a63d950e68e Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 17 Jan 2023 12:04:20 +0200 Subject: [PATCH 01/18] target .NET 6 --- src/AutoMapper/AutoMapper.csproj | 2 +- src/AutoMapper/Configuration/Profile.cs | 2 +- src/AutoMapper/Execution/ObjectFactory.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 41db321e10..3ab42ee93f 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -3,7 +3,7 @@ A convention-based object-object mapper. A convention-based object-object mapper. - netstandard2.1 + net6.0 true AutoMapper ..\..\AutoMapper.snk diff --git a/src/AutoMapper/Configuration/Profile.cs b/src/AutoMapper/Configuration/Profile.cs index 499b7fb33e..d2f247960f 100644 --- a/src/AutoMapper/Configuration/Profile.cs +++ b/src/AutoMapper/Configuration/Profile.cs @@ -180,6 +180,6 @@ public void IncludeSourceExtensionMethods(Type type) { _sourceExtensionMethods ??= new(); _sourceExtensionMethods.AddRange( - type.GetMethods(TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); + type.GetMethods(Internal.TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); } } \ No newline at end of file diff --git a/src/AutoMapper/Execution/ObjectFactory.cs b/src/AutoMapper/Execution/ObjectFactory.cs index 1f8ce014d2..d66f8d450b 100644 --- a/src/AutoMapper/Execution/ObjectFactory.cs +++ b/src/AutoMapper/Execution/ObjectFactory.cs @@ -19,7 +19,7 @@ private static Func GenerateConstructor(Type type) => }; private static Expression CallConstructor(Type type, IGlobalConfiguration configuration) { - var defaultCtor = type.GetConstructor(TypeExtensions.InstanceFlags, null, Type.EmptyTypes, null); + var defaultCtor = type.GetConstructor(Internal.TypeExtensions.InstanceFlags, null, Type.EmptyTypes, null); if (defaultCtor != null) { return New(defaultCtor); From 7bd1b81cde5667458739bb9b50889589f07ecae5 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 17 Jan 2023 12:26:22 +0200 Subject: [PATCH 02/18] nullable annotations for the runtime public API --- .../MapperConfigurationExpression.cs | 4 +-- src/AutoMapper/Mapper.cs | 29 ++++++++++--------- .../QueryableExtensions/Extensions.cs | 9 +++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs index 5c36306a8d..a4d2c3e237 100644 --- a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs @@ -184,7 +184,7 @@ private void AddMapsCore(IEnumerable assembliesToScan) { foreach (var memberConfigurationProvider in memberInfo.GetCustomAttributes().OfType()) { - mappingExpression.ForMember(memberInfo, cfg => memberConfigurationProvider.ApplyConfiguration(cfg)); + mappingExpression.ForMember(memberInfo, memberConfigurationProvider.ApplyConfiguration); } } @@ -195,5 +195,5 @@ private void AddMapsCore(IEnumerable assembliesToScan) AddProfile(autoMapAttributeProfile); } - public void ConstructServicesUsing(Func constructor) => _serviceCtor = constructor; + public void ConstructServicesUsing(Func constructor) => _serviceCtor = constructor ?? throw new ArgumentNullException(nameof(constructor)); } \ No newline at end of file diff --git a/src/AutoMapper/Mapper.cs b/src/AutoMapper/Mapper.cs index 960f4f78f5..adc9de4acc 100644 --- a/src/AutoMapper/Mapper.cs +++ b/src/AutoMapper/Mapper.cs @@ -1,7 +1,7 @@ namespace AutoMapper; - using IObjectMappingOperationOptions = IMappingOperationOptions; using Factory = Func; +#nullable enable public interface IMapperBase { /// @@ -11,7 +11,7 @@ public interface IMapperBase /// Destination type to create /// Source object to map from /// Mapped destination object - TDestination Map(object source); + TDestination? Map(object? source); /// /// Execute a mapping from the source object to a new destination object. /// @@ -19,7 +19,7 @@ public interface IMapperBase /// Destination type to create /// Source object to map from /// Mapped destination object - TDestination Map(TSource source); + TDestination? Map(TSource? source); /// /// Execute a mapping from the source object to the existing destination object. /// @@ -28,7 +28,7 @@ public interface IMapperBase /// Source object to map from /// Destination object to map into /// The mapped destination object, same instance as the object - TDestination Map(TSource source, TDestination destination); + TDestination? Map(TSource? source, TDestination? destination); /// /// Execute a mapping from the source object to a new destination object with explicit objects /// @@ -36,7 +36,7 @@ public interface IMapperBase /// Source type to use /// Destination type to create /// Mapped destination object - object Map(object source, Type sourceType, Type destinationType); + object? Map(object? source, Type? sourceType, Type destinationType); /// /// Execute a mapping from the source object to existing destination object with explicit objects /// @@ -45,7 +45,7 @@ public interface IMapperBase /// Source type to use /// Destination type to use /// Mapped destination object, same instance as the object - object Map(object source, object destination, Type sourceType, Type destinationType); + object? Map(object? source, object? destination, Type? sourceType, Type? destinationType); } public interface IMapper : IMapperBase { @@ -56,7 +56,7 @@ public interface IMapper : IMapperBase /// Source object to map from /// Mapping options /// Mapped destination object - TDestination Map(object source, Action> opts); + TDestination? Map(object? source, Action> opts); /// /// Execute a mapping from the source object to a new destination object with supplied mapping options. /// @@ -65,7 +65,7 @@ public interface IMapper : IMapperBase /// Source object to map from /// Mapping options /// Mapped destination object - TDestination Map(TSource source, Action> opts); + TDestination? Map(TSource? source, Action> opts); /// /// Execute a mapping from the source object to the existing destination object with supplied mapping options. /// @@ -75,7 +75,7 @@ public interface IMapper : IMapperBase /// Destination object to map into /// Mapping options /// The mapped destination object, same instance as the object - TDestination Map(TSource source, TDestination destination, Action> opts); + TDestination? Map(TSource? source, TDestination? destination, Action> opts); /// /// Execute a mapping from the source object to a new destination object with explicit objects and supplied mapping options. /// @@ -84,7 +84,7 @@ public interface IMapper : IMapperBase /// Destination type to create /// Mapping options /// Mapped destination object - object Map(object source, Type sourceType, Type destinationType, Action opts); + object? Map(object? source, Type? sourceType, Type destinationType, Action opts); /// /// Execute a mapping from the source object to existing destination object with supplied mapping options and explicit objects /// @@ -94,7 +94,7 @@ public interface IMapper : IMapperBase /// Destination type to use /// Mapping options /// Mapped destination object, same instance as the object - object Map(object source, object destination, Type sourceType, Type destinationType, Action opts); + object? Map(object? source, object? destination, Type? sourceType, Type? destinationType, Action opts); /// /// Configuration provider for performing maps /// @@ -108,7 +108,7 @@ public interface IMapper : IMapperBase /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Queryable result, use queryable extension methods to project and execute result - IQueryable ProjectTo(IQueryable source, object parameters = null, params Expression>[] membersToExpand); + IQueryable ProjectTo(IQueryable source, object? parameters = null, params Expression>[] membersToExpand); /// /// Project the input queryable. /// @@ -117,7 +117,7 @@ public interface IMapper : IMapperBase /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Queryable result, use queryable extension methods to project and execute result - IQueryable ProjectTo(IQueryable source, IDictionary parameters, params string[] membersToExpand); + IQueryable ProjectTo(IQueryable source, IDictionary? parameters, params string[] membersToExpand); /// /// Project the input queryable. /// @@ -126,11 +126,12 @@ public interface IMapper : IMapperBase /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Queryable result, use queryable extension methods to project and execute result - IQueryable ProjectTo(IQueryable source, Type destinationType, IDictionary parameters = null, params string[] membersToExpand); + IQueryable ProjectTo(IQueryable source, Type destinationType, IDictionary? parameters = null, params string[] membersToExpand); } public interface IRuntimeMapper : IMapperBase { } +#nullable disable internal interface IInternalRuntimeMapper : IRuntimeMapper { TDestination Map(TSource source, TDestination destination, ResolutionContext context, Type sourceType = null, Type destinationType = null, MemberMap memberMap = null); diff --git a/src/AutoMapper/QueryableExtensions/Extensions.cs b/src/AutoMapper/QueryableExtensions/Extensions.cs index 8b50d06579..05fea863bb 100644 --- a/src/AutoMapper/QueryableExtensions/Extensions.cs +++ b/src/AutoMapper/QueryableExtensions/Extensions.cs @@ -1,5 +1,4 @@ namespace AutoMapper.QueryableExtensions; - using MemberPaths = IEnumerable; using ParameterBag = IDictionary; /// @@ -20,7 +19,8 @@ static IQueryable Select(IQueryable source, LambdaExpression lambda) => source.P /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Expression to project into - public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, object parameters, params Expression>[] membersToExpand) => +#nullable enable + public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, object? parameters, params Expression>[] membersToExpand) => source.ToCore(configuration, parameters, membersToExpand.Select(MemberVisitor.GetMemberPath)); /// /// Extension method to project from a queryable using the provided mapping engine @@ -43,7 +43,7 @@ public static IQueryable ProjectTo(this IQueryable s /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Queryable result, use queryable extension methods to project and execute result - public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, ParameterBag parameters, params string[] membersToExpand) => + public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, ParameterBag? parameters, params string[] membersToExpand) => source.ToCore(configuration, parameters, membersToExpand.Select(memberName => ReflectionHelper.GetMemberPath(typeof(TDestination), memberName))); /// /// Extension method to project from a queryable using the provided mapping engine @@ -64,8 +64,9 @@ public static IQueryable ProjectTo(this IQueryable source, Type destinationType, /// Optional parameter object for parameterized mapping expressions /// Explicit members to expand /// Queryable result, use queryable extension methods to project and execute result - public static IQueryable ProjectTo(this IQueryable source, Type destinationType, IConfigurationProvider configuration, ParameterBag parameters, params string[] membersToExpand) => + public static IQueryable ProjectTo(this IQueryable source, Type destinationType, IConfigurationProvider configuration, ParameterBag? parameters, params string[] membersToExpand) => source.ToCore(destinationType, configuration, parameters, membersToExpand.Select(memberName => ReflectionHelper.GetMemberPath(destinationType, memberName))); +#nullable disable static IQueryable ToCore(this IQueryable source, IConfigurationProvider configuration, object parameters, MemberPaths memberPathsToExpand) => (IQueryable)source.ToCore(typeof(TResult), configuration, parameters, memberPathsToExpand); static IQueryable ToCore(this IQueryable source, Type destinationType, IConfigurationProvider configuration, object parameters, MemberPaths memberPathsToExpand) => From 1f7dfa6cd32cc264c358b471de34da326f74deb0 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Wed, 18 Jan 2023 08:46:53 +0200 Subject: [PATCH 03/18] Recursive queries --- docs/Queryable-Extensions.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/Queryable-Extensions.md b/docs/Queryable-Extensions.md index d01645086e..f2fffb3f7a 100644 --- a/docs/Queryable-Extensions.md +++ b/docs/Queryable-Extensions.md @@ -208,7 +208,15 @@ You may also use a dictionary to build the projection values: dbContext.Courses.ProjectTo(Config, new Dictionary { {"currentUserName", Request.User.Name} }); ``` -However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. +However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. + +### Recursive models + +Ideally, you would avoid models that reference themselves (do some research). But if you must, you need to enable them: + +```c# +configuration.Internal().RecursiveQueriesMaxDepth = someRandomNumber; +``` ### Supported mapping options From e92e87f95a2f7c281bdb9fdd55a6297bf99fc3fe Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sat, 21 Jan 2023 10:05:33 +0200 Subject: [PATCH 04/18] not needed in EF Core --- src/AutoMapper/PropertyMap.cs | 2 +- src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AutoMapper/PropertyMap.cs b/src/AutoMapper/PropertyMap.cs index 41639057ff..65cf1da42b 100644 --- a/src/AutoMapper/PropertyMap.cs +++ b/src/AutoMapper/PropertyMap.cs @@ -32,7 +32,7 @@ public PropertyMap(PropertyMap includedMemberMap, TypeMap typeMap, IncludedMembe public override string DestinationName => DestinationMember?.Name; public override Type DestinationType { get; protected set; } public override MemberInfo[] SourceMembers { get; set; } = Array.Empty(); - public override bool CanBeSet => ReflectionHelper.CanBeSet(DestinationMember); + public override bool CanBeSet => DestinationMember.CanBeSet(); public override bool Ignored { get; set; } public override Type SourceType => _sourceType ??= GetSourceType(); public void ApplyInheritedPropertyMap(PropertyMap inheritedMappedProperty) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index 586f65de10..d65e9850b0 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -86,10 +86,12 @@ bool OverMaxDepth() } void ProjectProperties() { - foreach (var propertyMap in typeMap.PropertyMaps.Where(pm => - pm.CanResolveValue && pm.DestinationMember.CanBeSet() && !typeMap.ConstructorParameterMatches(pm.DestinationName)) - .OrderBy(pm => pm.DestinationMember.MetadataToken)) + foreach (var propertyMap in typeMap.PropertyMaps) { + if (!propertyMap.CanResolveValue || !propertyMap.CanBeSet || typeMap.ConstructorParameterMatches(propertyMap.DestinationName)) + { + continue; + } var propertyProjection = TryProjectMember(propertyMap); if (propertyProjection != null) { From d409798b396107ab6e41d60f6d48a799dc13af62 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sat, 21 Jan 2023 10:15:49 +0200 Subject: [PATCH 05/18] remove --verbosity=normal --- Build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build.ps1 b/Build.ps1 index f3c142e64f..f78e52ab9b 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -26,6 +26,6 @@ $artifacts = ".\artifacts" if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } -exec { & dotnet test -c Release --results-directory $artifacts -l trx --verbosity=normal } +exec { & dotnet test -c Release --results-directory $artifacts -l trx } exec { & dotnet pack .\src\AutoMapper\AutoMapper.csproj -c Release -o $artifacts --no-build } From ed8bb627f8915c044963b1f155e2536cee7cb2df Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sun, 22 Jan 2023 17:28:21 +0200 Subject: [PATCH 06/18] keep TypeMap.Projection when using ForAllMaps --- .../Configuration/MappingExpression.cs | 3 ++- src/AutoMapper/ProfileMap.cs | 2 +- src/UnitTests/Enumerations.cs | 25 +++++++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/AutoMapper/Configuration/MappingExpression.cs b/src/AutoMapper/Configuration/MappingExpression.cs index 6bb950e180..f00da2fa21 100644 --- a/src/AutoMapper/Configuration/MappingExpression.cs +++ b/src/AutoMapper/Configuration/MappingExpression.cs @@ -1,7 +1,8 @@ namespace AutoMapper.Configuration; public class MappingExpression : MappingExpressionBase, IMappingExpression { - public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types){} + public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types){} + public MappingExpression(TypeMap typeMap) : this(typeMap.Types, typeMap.ConfiguredMemberList) => Projection = typeMap.Projection; public string[] IncludedMembersNames { get; internal set; } = Array.Empty(); public IMappingExpression ReverseMap() { diff --git a/src/AutoMapper/ProfileMap.cs b/src/AutoMapper/ProfileMap.cs index ddf92c71be..5bb0bd9275 100644 --- a/src/AutoMapper/ProfileMap.cs +++ b/src/AutoMapper/ProfileMap.cs @@ -185,7 +185,7 @@ private void Configure(TypeMap typeMap, IGlobalConfiguration configuration) } foreach (var action in AllTypeMapActions) { - var expression = new MappingExpression(typeMap.Types, typeMap.ConfiguredMemberList); + var expression = new MappingExpression(typeMap); action(typeMap, expression); expression.Configure(typeMap, configuration.SourceMembers); } diff --git a/src/UnitTests/Enumerations.cs b/src/UnitTests/Enumerations.cs index dd0ffaf58e..1d8b75a812 100644 --- a/src/UnitTests/Enumerations.cs +++ b/src/UnitTests/Enumerations.cs @@ -1,8 +1,29 @@ using System.Runtime.Serialization; using AutoMapper.UnitTests; - namespace AutoMapper.Tests; - +public class CreateProjectionEnum : AutoMapperSpecBase +{ + public class Source + { + public string Name { get; set; } + public SourceEnum Value { get; set; } + } + public class Dest + { + public string Name { get; set; } + public DestEnum Value { get; set; } + } + public enum SourceEnum { A, B } + public enum DestEnum { A, B } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateProjection().ConvertUsing(src => src == SourceEnum.A ? DestEnum.A : DestEnum.B); + c.CreateProjection(); + c.Internal().ForAllMaps(static (_, _) => { }); + }); + [Fact] + public void Should_work() => ProjectTo(new[] { new Source() }.AsQueryable()).Single().Value.ShouldBe(DestEnum.A); +} public class InvalidStringToEnum : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(_=> { }); From 2883ae81e90f55d6c1620fcaf1bae1cfff604b57 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Fri, 27 Jan 2023 13:55:28 +0200 Subject: [PATCH 07/18] inherit a MapFrom from an indirect base class --- src/AutoMapper/TypeMap.cs | 3 +- ...ncludedMappingShouldInheritBaseMappings.cs | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/AutoMapper/TypeMap.cs b/src/AutoMapper/TypeMap.cs index 08f981772d..e5e4398539 100644 --- a/src/AutoMapper/TypeMap.cs +++ b/src/AutoMapper/TypeMap.cs @@ -305,7 +305,8 @@ public void Seal(IGlobalConfiguration configuration, TypeMap thisMap) { foreach (var inheritedTypeMap in InheritedTypeMaps) { - var includedMaps = inheritedTypeMap?._details?.IncludedMembersTypeMaps; + inheritedTypeMap.Seal(configuration); + var includedMaps = inheritedTypeMap._details?.IncludedMembersTypeMaps; if (includedMaps != null) { IncludedMembersTypeMaps ??= new(); diff --git a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs index 66eb31db63..54dc32de8e 100644 --- a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs +++ b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs @@ -1,5 +1,34 @@ namespace AutoMapper.UnitTests; - +public class IncludeBaseIndirectBase : AutoMapperSpecBase +{ + public class FooBaseBase + { + } + public class FooBase : FooBaseBase + { + } + public class Foo : FooBase + { + } + public class FooDtoBaseBase + { + public DateTime Date { get; set; } + } + public class FooDtoBase : FooDtoBaseBase + { + } + public class FooDto : FooDtoBase + { + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().IncludeBase(); + c.CreateMap().IncludeBase(); + c.CreateMap().ForMember(d => d.Date, o => o.MapFrom(s => DateTime.MaxValue)); + }); + [Fact] + public void Should_work() => Map(new Foo()).Date.ShouldBe(DateTime.MaxValue); +} public class ReadonlyCollectionPropertiesOverride : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(cfg => From b4626e6a4e590f59978a63e800cce5eb3c2297da Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sun, 12 Feb 2023 10:32:49 +0200 Subject: [PATCH 08/18] To skip validation altogether for this map, use `MemberList.None`. That's the default for `ReverseMap`. --- docs/Configuration-validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration-validation.md b/docs/Configuration-validation.md index 2f0efc7386..fb99659cce 100644 --- a/docs/Configuration-validation.md +++ b/docs/Configuration-validation.md @@ -50,7 +50,7 @@ var configuration = new MapperConfiguration(cfg => ); ``` -To skip validation altogether for this map, use `MemberList.None`. +To skip validation altogether for this map, use `MemberList.None`. That's the default for `ReverseMap`. ## Custom validations From fd52b952318292a1d0bba3d0cf6a75a1ec613f5f Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 14 Feb 2023 10:48:15 +0200 Subject: [PATCH 09/18] require either destination or destinationType --- src/AutoMapper/Mapper.cs | 21 +++++++++++++++++---- src/UnitTests/NullBehavior.cs | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/AutoMapper/Mapper.cs b/src/AutoMapper/Mapper.cs index adc9de4acc..132b0e0524 100644 --- a/src/AutoMapper/Mapper.cs +++ b/src/AutoMapper/Mapper.cs @@ -165,10 +165,23 @@ public TDestination Map(TSource source, TDestination dest public object Map(object source, Type sourceType, Type destinationType) => Map(source, null, sourceType, destinationType); public object Map(object source, Type sourceType, Type destinationType, Action opts) => Map(source, null, sourceType, destinationType, opts); - public object Map(object source, object destination, Type sourceType, Type destinationType) => - MapCore(source, destination, DefaultContext, sourceType, destinationType); - public object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) => - MapWithOptions(source, destination, opts, sourceType, destinationType); + public object Map(object source, object destination, Type sourceType, Type destinationType) + { + CheckDestination(destination, destinationType); + return MapCore(source, destination, DefaultContext, sourceType, destinationType); + } + private static void CheckDestination(object destination, Type destinationType) + { + if (destination == null && destinationType == null) + { + throw new ArgumentNullException(nameof(destinationType)); + } + } + public object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) + { + CheckDestination(destination, destinationType); + return MapWithOptions(source, destination, opts, sourceType, destinationType); + } public IQueryable ProjectTo(IQueryable source, object parameters, params Expression>[] membersToExpand) => source.ProjectTo(ConfigurationProvider, parameters, membersToExpand); public IQueryable ProjectTo(IQueryable source, IDictionary parameters, params string[] membersToExpand) diff --git a/src/UnitTests/NullBehavior.cs b/src/UnitTests/NullBehavior.cs index 7d29dca688..59faf94037 100644 --- a/src/UnitTests/NullBehavior.cs +++ b/src/UnitTests/NullBehavior.cs @@ -1,4 +1,22 @@ namespace AutoMapper.UnitTests.NullBehavior; +public class NullDestinationType : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(c => { }); + [Fact] + public void Should_require_destination_object() + { + new Action(() => Mapper.Map("", null, null)).ShouldThrow().ParamName.ShouldBe("destinationType"); + new Action(() => Mapper.Map("", null, null, _=>{ })).ShouldThrow().ParamName.ShouldBe("destinationType"); + Mapper.Map("", "", null, null).ShouldBe(""); + Mapper.Map("", null, null, typeof(string)).ShouldBe(""); + Mapper.Map("", "", null, null, _ => { }).ShouldBe(""); + Mapper.Map("", null, null, typeof(string), _=>{ }).ShouldBe(""); + Mapper.Map("").ShouldBe(""); + Mapper.Map("", default(string)).ShouldBe(""); + Mapper.Map("", _ => { }).ShouldBe(""); + Mapper.Map("", default(string), _ => { }).ShouldBe(""); + } +} public class NullToExistingDestination : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap().DisableCtorValidation()); From 64b79b1929b619c4fbe44ad21e9b0969cb55dfb9 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 14 Feb 2023 11:17:45 +0200 Subject: [PATCH 10/18] 13.0 Upgrade Guide --- docs/13.0-Upgrade-Guide.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/13.0-Upgrade-Guide.md diff --git a/docs/13.0-Upgrade-Guide.md b/docs/13.0-Upgrade-Guide.md new file mode 100644 index 0000000000..3ae61af7bf --- /dev/null +++ b/docs/13.0-Upgrade-Guide.md @@ -0,0 +1,9 @@ +# 13.0 Upgrade Guide + +[Release notes](https://github.com/AutoMapper/AutoMapper/releases/tag/v13.0.0). + +## AutoMapper now targets .Net 6 + +## `IMapper` has nullable annotations + +Besides the build-time impact, there is also a behaviour change. Non-generic `Map` overloads require now either a destination type or a non-null destination object. \ No newline at end of file From 3ace993a3a0b2503a372970909a387aaddffb869 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Fri, 17 Feb 2023 09:55:52 +0200 Subject: [PATCH 11/18] cast the type map expression to the property type --- src/AutoMapper/Execution/ExpressionBuilder.cs | 3 +-- src/UnitTests/TypeConverters.cs | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/AutoMapper/Execution/ExpressionBuilder.cs b/src/AutoMapper/Execution/ExpressionBuilder.cs index 7ffd081c30..75e160a64d 100644 --- a/src/AutoMapper/Execution/ExpressionBuilder.cs +++ b/src/AutoMapper/Execution/ExpressionBuilder.cs @@ -87,14 +87,13 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, { mapExpression = mapper.MapExpression(configuration, profileMap, memberMap, source, destination); nullCheck = mapExpression != source; - mapExpression = ToType(mapExpression, typePair.DestinationType); } else { nullCheck = true; } } - mapExpression ??= ContextMap(typePair, source, destination, memberMap); + mapExpression = mapExpression == null ? ContextMap(typePair, source, destination, memberMap) : ToType(mapExpression, typePair.DestinationType); return nullCheck ? configuration.NullCheckSource(profileMap, source, destination, mapExpression, memberMap) : mapExpression; } public static Expression NullCheckSource(this IGlobalConfiguration configuration, ProfileMap profileMap, Expression source, Expression destination, diff --git a/src/UnitTests/TypeConverters.cs b/src/UnitTests/TypeConverters.cs index d05f334c8c..c18e51895e 100644 --- a/src/UnitTests/TypeConverters.cs +++ b/src/UnitTests/TypeConverters.cs @@ -1,5 +1,26 @@ namespace AutoMapper.UnitTests.CustomMapping; - +public class StringToEnumConverter : AutoMapperSpecBase +{ + class Source + { + public string Enum { get; set; } + } + class Destination + { + public ConsoleColor Enum { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ConvertUsing(s => ConsoleColor.DarkCyan); + c.CreateMap(); + }); + [Fact] + public void Should_work() + { + Map("").ShouldBe(ConsoleColor.DarkCyan); + Map(new Source()).Enum.ShouldBe(ConsoleColor.DarkCyan); + } +} public class NullableConverter : AutoMapperSpecBase { public enum GreekLetters From 12a78d1ecd95475b7b3969d2f7c1153bb0fe78a3 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sun, 26 Feb 2023 11:13:52 +0200 Subject: [PATCH 12/18] keep the source type in sync with the resolver --- src/AutoMapper/ConstructorMap.cs | 2 -- src/AutoMapper/MemberMap.cs | 6 +++++- src/AutoMapper/PropertyMap.cs | 2 -- src/UnitTests/ForAllMembers.cs | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/AutoMapper/ConstructorMap.cs b/src/AutoMapper/ConstructorMap.cs index c79e445b25..f6d9122dbc 100644 --- a/src/AutoMapper/ConstructorMap.cs +++ b/src/AutoMapper/ConstructorMap.cs @@ -70,7 +70,6 @@ public bool ApplyIncludedMember(IncludedMember includedMember) [EditorBrowsable(EditorBrowsableState.Never)] public class ConstructorParameterMap : MemberMap { - private Type _sourceType; public ConstructorParameterMap(TypeMap typeMap, ParameterInfo parameter, MemberInfo[] sourceMembers) : base(typeMap) { Parameter = parameter; @@ -87,7 +86,6 @@ public ConstructorParameterMap(ConstructorParameterMap parameterMap, IncludedMem this(includedMember.TypeMap, parameterMap.Parameter, parameterMap.SourceMembers) => IncludedMember = includedMember.Chain(parameterMap.IncludedMember); public ParameterInfo Parameter { get; } - public override Type SourceType => _sourceType ??= GetSourceType(); public override Type DestinationType => Parameter.ParameterType; public override IncludedMember IncludedMember { get; } public override MemberInfo[] SourceMembers { get; set; } diff --git a/src/AutoMapper/MemberMap.cs b/src/AutoMapper/MemberMap.cs index c1a5a3dcf8..3e3d179d2f 100644 --- a/src/AutoMapper/MemberMap.cs +++ b/src/AutoMapper/MemberMap.cs @@ -1,3 +1,5 @@ +using System.Security.AccessControl; + namespace AutoMapper; /// /// The base class for member maps (property, constructor and path maps). @@ -5,6 +7,7 @@ namespace AutoMapper; [EditorBrowsable(EditorBrowsableState.Never)] public class MemberMap : IValueResolver { + private protected Type _sourceType; protected MemberMap(TypeMap typeMap = null) => TypeMap = typeMap; internal static readonly MemberMap Instance = new(); public TypeMap TypeMap { get; protected set; } @@ -14,9 +17,10 @@ public class MemberMap : IValueResolver public void SetResolver(IValueResolver resolver) { Resolver = resolver; + _sourceType = resolver.ResolvedType; Ignored = false; } - public virtual Type SourceType => default; + public virtual Type SourceType => _sourceType ??= GetSourceType(); public virtual MemberInfo[] SourceMembers { get => Array.Empty(); set { } } public virtual IncludedMember IncludedMember => null; public virtual string DestinationName => default; diff --git a/src/AutoMapper/PropertyMap.cs b/src/AutoMapper/PropertyMap.cs index 65cf1da42b..c2fc12fbd8 100644 --- a/src/AutoMapper/PropertyMap.cs +++ b/src/AutoMapper/PropertyMap.cs @@ -5,7 +5,6 @@ namespace AutoMapper; public class PropertyMap : MemberMap { private MemberMapDetails _details; - private Type _sourceType; public PropertyMap(MemberInfo destinationMember, Type destinationMemberType, TypeMap typeMap) : base(typeMap) { DestinationMember = destinationMember; @@ -34,7 +33,6 @@ public PropertyMap(PropertyMap includedMemberMap, TypeMap typeMap, IncludedMembe public override MemberInfo[] SourceMembers { get; set; } = Array.Empty(); public override bool CanBeSet => DestinationMember.CanBeSet(); public override bool Ignored { get; set; } - public override Type SourceType => _sourceType ??= GetSourceType(); public void ApplyInheritedPropertyMap(PropertyMap inheritedMappedProperty) { if (Ignored) diff --git a/src/UnitTests/ForAllMembers.cs b/src/UnitTests/ForAllMembers.cs index e24001f4d0..b8e5890a2e 100644 --- a/src/UnitTests/ForAllMembers.cs +++ b/src/UnitTests/ForAllMembers.cs @@ -76,4 +76,28 @@ public void Should_use_resolver() dest.SomeDate.ShouldBe(source.SomeDate); dest.OtherDate.ShouldBe(source.OtherDate.AddDays(1)); } +} +public class ForAllPropertyMaps_ConvertUsing : AutoMapperSpecBase +{ + public class Well + { + public SpecialTags SpecialTags { get; set; } + } + [Flags] + public enum SpecialTags { None, SendState, NotSendZeroWhenOpen } + public class PostPutWellViewModel + { + public SpecialTags[] SpecialTags { get; set; } = Array.Empty(); + } + class EnumToArray : IValueConverter + { + public object Convert(object sourceMember, ResolutionContext context) => new[] { SpecialTags.SendState }; + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.Internal().ForAllPropertyMaps(pm => pm.SourceType != null, (tm, mapper) => mapper.ConvertUsing(new EnumToArray())); + }); + [Fact] + public void ShouldWork() => Map(new Well()).SpecialTags.Single().ShouldBe(SpecialTags.SendState); } \ No newline at end of file From 90a6580c784a54cd61844124ae6c40c1a29ed382 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Sun, 26 Feb 2023 12:00:29 +0200 Subject: [PATCH 13/18] less allocations --- src/AutoMapper/ApiCompatBaseline.txt | 5 ++++- src/AutoMapper/Configuration/Profile.cs | 14 ++++++-------- src/AutoMapper/ProfileMap.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/AutoMapper/ApiCompatBaseline.txt b/src/AutoMapper/ApiCompatBaseline.txt index 90da3f7e58..cdebaea815 100644 --- a/src/AutoMapper/ApiCompatBaseline.txt +++ b/src/AutoMapper/ApiCompatBaseline.txt @@ -1,5 +1,8 @@ Compat issues with assembly AutoMapper: CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.AutoMapAttribute' changed from '[AttributeUsageAttribute(1036, AllowMultiple=true)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple=true)]' in the implementation. +InterfacesShouldHaveSameMembers : Interface member 'public System.Collections.Generic.IReadOnlyCollection AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Collections.Generic.IReadOnlyCollection> AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyCollection> AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.Configuration.ICtorParamConfigurationExpression.ExplicitExpansion()' is present in the implementation but not in the contract. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.IgnoreAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MapAtRuntimeAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. @@ -10,4 +13,4 @@ CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMappe CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueConverterAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueResolverAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. TypeCannotChangeClassification : Type 'AutoMapper.Execution.TypeMapPlanBuilder' is a 'ref struct' in the implementation but is a 'struct' in the contract. -Total Issues: 11 +Total Issues: 14 diff --git a/src/AutoMapper/Configuration/Profile.cs b/src/AutoMapper/Configuration/Profile.cs index d2f247960f..30b1f51899 100644 --- a/src/AutoMapper/Configuration/Profile.cs +++ b/src/AutoMapper/Configuration/Profile.cs @@ -10,7 +10,7 @@ public interface IProfileConfiguration bool? AllowNullCollections { get; } bool? EnableNullPropagationForQueryMapping { get; } IReadOnlyCollection> AllTypeMapActions { get; } - IReadOnlyCollection> AllPropertyMapActions { get; } + IReadOnlyCollection AllPropertyMapActions { get; } /// /// Source extension methods included for search @@ -60,7 +60,7 @@ public class Profile : IProfileExpressionInternal, IProfileConfiguration private readonly PrePostfixName _prePostfixName = new(); private ReplaceName _replaceName; private readonly MemberConfiguration _memberConfiguration; - private List> _allPropertyMapActions; + private List _allPropertyMapActions; private List> _allTypeMapActions; private List _globalIgnores; private List _openTypeMapConfigs; @@ -81,7 +81,7 @@ protected Profile() bool? IProfileExpressionInternal.FieldMappingEnabled { get; set; } bool? IProfileConfiguration.FieldMappingEnabled => this.Internal().FieldMappingEnabled; bool? IProfileConfiguration.EnableNullPropagationForQueryMapping => this.Internal().EnableNullPropagationForQueryMapping; - IReadOnlyCollection> IProfileConfiguration.AllPropertyMapActions + IReadOnlyCollection IProfileConfiguration.AllPropertyMapActions => _allPropertyMapActions.NullCheck(); IReadOnlyCollection> IProfileConfiguration.AllTypeMapActions => _allTypeMapActions.NullCheck(); IReadOnlyCollection IProfileConfiguration.GlobalIgnores => _globalIgnores.NullCheck(); @@ -122,10 +122,7 @@ void IProfileExpressionInternal.ForAllMaps(Action c void IProfileExpressionInternal.ForAllPropertyMaps(Func condition, Action configuration) { _allPropertyMapActions ??= new(); - _allPropertyMapActions.Add((pm, cfg) => - { - if (condition(pm)) configuration(pm, cfg); - }); + _allPropertyMapActions.Add(new(condition, configuration)); } public IProjectionExpression CreateProjection() => CreateProjection(MemberList.Destination); @@ -182,4 +179,5 @@ public void IncludeSourceExtensionMethods(Type type) _sourceExtensionMethods.AddRange( type.GetMethods(Internal.TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); } -} \ No newline at end of file +} +public readonly record struct PropertyMapAction(Func Condition, Action Action); \ No newline at end of file diff --git a/src/AutoMapper/ProfileMap.cs b/src/AutoMapper/ProfileMap.cs index 5bb0bd9275..8e1d01e2b0 100644 --- a/src/AutoMapper/ProfileMap.cs +++ b/src/AutoMapper/ProfileMap.cs @@ -88,7 +88,7 @@ internal void Clear() public Func ShouldMapProperty { get; } public Func ShouldMapMethod { get; } public Func ShouldUseConstructor { get; } - public IEnumerable> AllPropertyMapActions { get; } + public IEnumerable AllPropertyMapActions { get; } public IEnumerable> AllTypeMapActions { get; } public HashSet GlobalIgnores { get; } public MemberConfiguration MemberConfiguration { get; } @@ -192,9 +192,13 @@ private void Configure(TypeMap typeMap, IGlobalConfiguration configuration) foreach (var action in AllPropertyMapActions) { foreach (var propertyMap in typeMap.PropertyMaps) - { + { + if (!action.Condition(propertyMap)) + { + continue; + } var memberExpression = new MemberConfigurationExpression(propertyMap.DestinationMember, typeMap.SourceType); - action(propertyMap, memberExpression); + action.Action(propertyMap, memberExpression); memberExpression.Configure(typeMap); } } From c862b54a5eab1b43e51f10e79ba2416eee088c37 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 14 Mar 2023 08:09:15 +0200 Subject: [PATCH 14/18] better error --- src/AutoMapper/Execution/ExpressionBuilder.cs | 1 + .../Projection/ProjectCollectionListTest.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/AutoMapper/Execution/ExpressionBuilder.cs b/src/AutoMapper/Execution/ExpressionBuilder.cs index 75e160a64d..017a54d2e1 100644 --- a/src/AutoMapper/Execution/ExpressionBuilder.cs +++ b/src/AutoMapper/Execution/ExpressionBuilder.cs @@ -68,6 +68,7 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, bool nullCheck; if (typeMap != null) { + typeMap.CheckProjection(); var allowNull = memberMap?.AllowNull; nullCheck = !typeMap.HasTypeConverter && (destination.NodeType != ExpressionType.Default || (allowNull.HasValue && allowNull != profileMap.AllowNullDestinationValues)); diff --git a/src/UnitTests/Projection/ProjectCollectionListTest.cs b/src/UnitTests/Projection/ProjectCollectionListTest.cs index c6521aa244..c9264d709d 100644 --- a/src/UnitTests/Projection/ProjectCollectionListTest.cs +++ b/src/UnitTests/Projection/ProjectCollectionListTest.cs @@ -87,3 +87,26 @@ public override bool Equals(object obj) } } } +public class MapProjection : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateMap(); + }); + [Fact] + public void ShouldNotMap() => new Action(() => Map(new Customer())).ShouldThrow().Message.ShouldBe("CreateProjection works with ProjectTo, not with Map."); + public class Customer + { + public IList
Addresses { get; set; } + } + public record class Address(string Street); + public class CustomerDto + { + public IList Addresses { get; set; } + } + public class AddressDto + { + public string Street { get; set; } + } +} \ No newline at end of file From 1dbb7f77c2e476b1b608c69f7242329330f06ade Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Wed, 22 Mar 2023 14:52:36 +0200 Subject: [PATCH 15/18] simple MapFrom expressions are already chained from the entity parameter --- .../QueryableExtensions/ProjectionBuilder.cs | 13 +-- .../MapObjectPropertyFromSubQuery.cs | 89 ++++++++++++++++++- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index d65e9850b0..cd8f58c225 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -262,18 +262,7 @@ public Expression GetSourceExpression(Expression parameter) for (int index = 0; index < Members.Length - 1; index++) { var sourceMember = Members[index].Expression; - if (sourceMember is LambdaExpression lambda) - { - sourceExpression = lambda.ReplaceParameters(sourceExpression); - } - else - { - var chain = sourceMember.GetChain(); - if (chain.TryPeek(out var first)) - { - sourceExpression = sourceMember.Replace(first.Target, sourceExpression); - } - } + sourceExpression = sourceMember is LambdaExpression lambda ? lambda.ReplaceParameters(sourceExpression) : sourceMember; } return sourceExpression; } diff --git a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs index 0e2d5d7280..4d801e5e99 100644 --- a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs +++ b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs @@ -1,5 +1,92 @@ namespace AutoMapper.IntegrationTests.CustomMapFrom; - +public class MultipleLevelsSubquery : IntegrationTest +{ + [Fact] + public void Should_work() + { + using var context = new Context(); + var resultQuery = ProjectTo(context.Foos); + resultQuery.Single().MyBar.MyBaz.FirstWidget.Id.ShouldBe(1); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ForMember(f => f.MyBar, opts => opts.MapFrom(src => src.Bar)); + c.CreateMap().ForMember(f => f.MyBaz, opts => opts.MapFrom(src => src.Baz)); + c.CreateMap().ForMember(f => f.FirstWidget, opts => opts.MapFrom(src => src.Widgets.FirstOrDefault())); + c.CreateMap(); + }); + public class Context : LocalDbContext + { + public virtual DbSet Foos { get; set; } + public virtual DbSet Bazs { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var testBaz = new Baz(); + testBaz.Widgets.Add(new Widget()); + testBaz.Widgets.Add(new Widget()); + var testBar = new Bar(); + testBar.Foos.Add(new Foo()); + testBaz.Bars.Add(testBar); + context.Bazs.Add(testBaz); + } + } + public class Foo + { + public int Id { get; set; } + public int BarId { get; set; } + public virtual Bar Bar { get; set; } + } + public class Bar + { + public Bar() => Foos = new HashSet(); + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + public virtual ICollection Foos { get; set; } + } + public class Baz + { + public Baz() + { + Bars = new HashSet(); + Widgets = new HashSet(); + } + public int Id { get; set; } + public virtual ICollection Bars { get; set; } + public virtual ICollection Widgets { get; set; } + } + public partial class Widget + { + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + } + public class FooModel + { + public int Id { get; set; } + public int BarId { get; set; } + public BarModel MyBar { get; set; } + } + public class BarModel + { + public int Id { get; set; } + public int BazId { get; set; } + public BazModel MyBaz { get; set; } + } + public class BazModel + { + public int Id { get; set; } + public WidgetModel FirstWidget { get; set; } + } + public class WidgetModel + { + public int Id { get; set; } + public int BazId { get; set; } + } +} public class MemberWithSubQueryProjections : IntegrationTest { public class Customer From 22f9973e8abca74b3f4aa611c51202f239f36092 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Thu, 30 Mar 2023 12:51:00 +0300 Subject: [PATCH 16/18] cosmetic --- docs/index.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 24f0f4d003..304490d9aa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,10 +11,6 @@ other simple objects, whose design is better suited for serialization, communication, messaging, or simply an anti-corruption layer between the domain and application layer. -AutoMapper supports the following platforms: - -* `.NET Standard 2.1+ `_ - New to AutoMapper? Check out the :doc:`Getting-started` page first. .. _user-docs: From 99eb95bac6d90e05ff4af640263817facd0731af Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Tue, 4 Apr 2023 11:59:34 +0300 Subject: [PATCH 17/18] constructor mapping for structs with ProjectTo --- .../QueryableExtensions/ProjectionBuilder.cs | 2 +- .../ConstructorDefaultValue.cs | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index cd8f58c225..1c12eab207 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -125,7 +125,7 @@ Expression ProjectMemberCore() resolvedSource is not ParameterExpression && !resolvedSource.Type.IsCollection()) { // Handles null source property so it will not create an object with possible non-nullable properties which would result in an exception. - mappedExpression = resolvedSource.IfNullElse(Constant(null, mappedExpression.Type), mappedExpression); + mappedExpression = resolvedSource.IfNullElse(Default(mappedExpression.Type), mappedExpression); } } else diff --git a/src/IntegrationTests/ConstructorDefaultValue.cs b/src/IntegrationTests/ConstructorDefaultValue.cs index c580d8346a..d99f749d64 100644 --- a/src/IntegrationTests/ConstructorDefaultValue.cs +++ b/src/IntegrationTests/ConstructorDefaultValue.cs @@ -29,4 +29,39 @@ public void Can_map_with_projection() using var context = new Context(); ProjectTo(context.Customers).Single().Value.ShouldBe(5); } +} +public class StructConstructorMapping : IntegrationTest +{ + public class Customer + { + public int Id { get; set; } + public DateTime Date { get; set; } + } + public class CustomerViewModel + { + public DateOnly Date { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new Customer { Date = new(1984, 5, 23) }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateProjection(); + }); + [Fact] + public void Can_map_with_projection() + { + using var context = new Context(); + ProjectTo(context.Customers).Single().Date.ShouldBe(new(1984, 5, 23)); + } } \ No newline at end of file From 8e537105fc3f4345761fa77b37d3faa126c6d522 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Wed, 5 Apr 2023 12:35:43 +0300 Subject: [PATCH 18/18] remove AllowAdditiveTypeMapCreation --- docs/13.0-Upgrade-Guide.md | 6 ++++- src/AutoMapper/AutoMapperMappingException.cs | 6 ++--- .../Configuration/ConfigurationValidator.cs | 21 +++++++-------- .../MapperConfigurationExpression.cs | 7 ----- src/AutoMapper/Internal/InternalApi.cs | 5 ---- src/UnitTests/ConfigurationRules.cs | 26 ------------------- 6 files changed, 16 insertions(+), 55 deletions(-) diff --git a/docs/13.0-Upgrade-Guide.md b/docs/13.0-Upgrade-Guide.md index 3ae61af7bf..407224dd7d 100644 --- a/docs/13.0-Upgrade-Guide.md +++ b/docs/13.0-Upgrade-Guide.md @@ -6,4 +6,8 @@ ## `IMapper` has nullable annotations -Besides the build-time impact, there is also a behaviour change. Non-generic `Map` overloads require now either a destination type or a non-null destination object. \ No newline at end of file +Besides the build-time impact, there is also a behaviour change. Non-generic `Map` overloads require now either a destination type or a non-null destination object. + +## `AllowAdditiveTypeMapCreation` was removed + +Be sure to call `CreateMap` once for a source type, destination type pair. If you want to reuse configuration, use mapping inheritance. \ No newline at end of file diff --git a/src/AutoMapper/AutoMapperMappingException.cs b/src/AutoMapper/AutoMapperMappingException.cs index 2be71cd072..e167e2927d 100644 --- a/src/AutoMapper/AutoMapperMappingException.cs +++ b/src/AutoMapper/AutoMapperMappingException.cs @@ -81,15 +81,13 @@ public DuplicateTypeMapConfigurationException(TypeMapConfigErrors[] errors) { Errors = errors; var builder = new StringBuilder(); - builder.AppendLine("The following type maps were found in multiple profiles:"); + builder.AppendLine("Duplicate CreateMap calls:"); foreach (var error in Errors) { builder.AppendLine($"{error.Types.SourceType.FullName} to {error.Types.DestinationType.FullName} defined in profiles:"); builder.AppendLine(string.Join(Environment.NewLine, error.ProfileNames)); } - builder.AppendLine("This can cause configuration collisions and inconsistent mapping."); - builder.AppendLine("Consolidate the CreateMap calls into one profile, or set the root Internal().AllowAdditiveTypeMapCreation configuration value to 'true'."); - + builder.AppendLine("This can cause configuration collisions and inconsistent mappings. Use a single CreateMap call per type pair."); Message = builder.ToString(); } diff --git a/src/AutoMapper/Configuration/ConfigurationValidator.cs b/src/AutoMapper/Configuration/ConfigurationValidator.cs index ecc5090767..470655def7 100644 --- a/src/AutoMapper/Configuration/ConfigurationValidator.cs +++ b/src/AutoMapper/Configuration/ConfigurationValidator.cs @@ -13,19 +13,16 @@ private void Validate(ValidationContext context) } public void AssertConfigurationExpressionIsValid(IGlobalConfiguration config, IEnumerable typeMaps) { - if (!Expression.AllowAdditiveTypeMapCreation) + var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) + .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) + .GroupBy(x => x.typeMap.Types) + .Where(g => g.Count() > 1) + .Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray())) + .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) + .ToArray(); + if (duplicateTypeMapConfigs.Any()) { - var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) - .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) - .GroupBy(x => x.typeMap.Types) - .Where(g => g.Count() > 1) - .Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray())) - .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) - .ToArray(); - if (duplicateTypeMapConfigs.Any()) - { - throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); - } + throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); } AssertConfigurationIsValid(config, typeMaps); } diff --git a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs index a4d2c3e237..14588f6b58 100644 --- a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs @@ -101,13 +101,6 @@ public class MapperConfigurationExpression : Profile, IGlobalConfigurationExpres /// the validation callback void IGlobalConfigurationExpression.Validator(Validator validator) => _validators.Add(validator ?? throw new ArgumentNullException(nameof(validator))); - - /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool IGlobalConfigurationExpression.AllowAdditiveTypeMapCreation { get; set; } - /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. diff --git a/src/AutoMapper/Internal/InternalApi.cs b/src/AutoMapper/Internal/InternalApi.cs index 5be71f4b05..fe0362d948 100644 --- a/src/AutoMapper/Internal/InternalApi.cs +++ b/src/AutoMapper/Internal/InternalApi.cs @@ -32,11 +32,6 @@ public interface IGlobalConfigurationExpression : IMapperConfigurationExpression /// the validation callback void Validator(Validator validator); /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool AllowAdditiveTypeMapCreation { get; set; } - /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. /// diff --git a/src/UnitTests/ConfigurationRules.cs b/src/UnitTests/ConfigurationRules.cs index b7f390d725..e297f6d2e2 100644 --- a/src/UnitTests/ConfigurationRules.cs +++ b/src/UnitTests/ConfigurationRules.cs @@ -33,19 +33,6 @@ public void Should_throw_for_multiple_create_map_calls() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls() - { - var config = new MapperConfiguration(cfg => - { - cfg.CreateMap(); - cfg.CreateMap(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_different_profiles() { @@ -58,19 +45,6 @@ public void Should_throw_for_multiple_create_map_calls_in_different_profiles() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls_in_different_profiles() - { - var config = new MapperConfiguration(cfg => - { - cfg.AddProfile(); - cfg.AddProfile(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_configuration_expression_and_profile() {