diff --git a/src/Umbraco.Core/Mapping/Mapper.cs b/src/Umbraco.Core/Mapping/Mapper.cs index 27b651f155..8d89585bfa 100644 --- a/src/Umbraco.Core/Mapping/Mapper.cs +++ b/src/Umbraco.Core/Mapping/Mapper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,8 @@ namespace Umbraco.Core.Mapping public class Mapper { - private readonly Dictionary>> _maps = new Dictionary>>(); + private readonly Dictionary>> _ctors = new Dictionary>>(); + private readonly Dictionary>> _maps = new Dictionary>>(); public Mapper(MapperProfileCollection profiles) { @@ -19,15 +21,29 @@ namespace Umbraco.Core.Mapping profile.SetMaps(this); } - public void SetMap(Func map) + public void SetMap() + => SetMap((source, target) => { }); + + public void SetMap(Action map) + => SetMap(source => throw new NotSupportedException($"Don't know how to create {typeof(TTarget)} instances."), map); + + public void SetMap(Func ctor) + => SetMap(ctor, (source, target) => { }); + + public void SetMap(Func ctor, Action map) { var sourceType = typeof(TSource); var targetType = typeof(TTarget); - if (!_maps.TryGetValue(sourceType, out var sourceMap)) - sourceMap = _maps[sourceType] = new Dictionary>(); + if (!_ctors.TryGetValue(sourceType, out var sourceCtor)) + sourceCtor = _ctors[sourceType] = new Dictionary>(); - sourceMap[targetType] = o => map((TSource)o); + sourceCtor[targetType] = source => ctor((TSource) source); + + if (!_maps.TryGetValue(sourceType, out var sourceMap)) + sourceMap = _maps[sourceType] = new Dictionary>(); + + sourceMap[targetType] = (source, target) => map((TSource) source, (TTarget) target); } public TTarget Map(object source) @@ -35,18 +51,161 @@ namespace Umbraco.Core.Mapping if (source == null) throw new ArgumentNullException(nameof(source)); + var sourceType = source.GetType(); + var targetType = typeof(TTarget); + + var ctor = GetCtor(sourceType, typeof(TTarget)); + var map = GetMap(sourceType, typeof(TTarget)); + if (ctor != null && map != null) + { + var target = ctor(source); + map(source, target); + return (TTarget)target; + } + + bool IsOk(Type type) + { + // note: we're not going to work with just plain enumerables of anything, + // only on arrays or anything that is generic and implements IEnumerable<> + + if (type.IsArray && type.GetArrayRank() == 1) return true; + if (type.IsGenericType && type.GenericTypeArguments.Length == 1) return true; + return false; + } + + Type GetGenericArg(Type type) + { + if (type.IsArray) return type.GetElementType(); + if (type.IsGenericType) return type.GenericTypeArguments[0]; + throw new Exception("panic"); + } + + if (IsOk(sourceType) && IsOk(targetType)) + { + var sourceGenericArg = GetGenericArg(sourceType); + var targetGenericArg = GetGenericArg(targetType); + + var sourceEnumerableType = typeof(IEnumerable<>).MakeGenericType(sourceGenericArg); + var targetEnumerableType = typeof(IEnumerable<>).MakeGenericType(targetGenericArg); + + if (sourceEnumerableType.IsAssignableFrom(sourceType) && targetEnumerableType.IsAssignableFrom(targetType)) + { + ctor = GetCtor(sourceGenericArg, targetGenericArg); + map = GetMap(sourceGenericArg, targetGenericArg); + + if (ctor != null && map != null) + { + var sourceEnumerable = (IEnumerable)source; + var targetEnumerable = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); + + foreach (var sourceItem in sourceEnumerable) + { + var targetItem = ctor(sourceItem); + map(sourceItem, targetItem); + targetEnumerable.Add(targetItem); + } + + return (TTarget)targetEnumerable; + } + + // fixme - temp + return AutoMapper.Mapper.Map(source); + } + } + + // fixme this is temp + //throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}."); + return AutoMapper.Mapper.Map(source); + } + + // TODO: when AutoMapper is completely gone these two methods can merge + + public TTarget Map(TSource source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var sourceType = typeof(TSource); + var targetType = typeof(TTarget); + + var ctor = GetCtor(sourceType, typeof(TTarget)); + var map = GetMap(sourceType, targetType); + if (ctor != null && map != null) + { + var target = ctor(source); + map(source, target); + return (TTarget) target; + } + + if (sourceType.IsGenericType && targetType.IsGenericType) + { + var sourceGeneric = sourceType.GetGenericTypeDefinition(); + var targetGeneric = targetType.GetGenericTypeDefinition(); + var ienumerable = typeof(IEnumerable<>); + + if (sourceGeneric == ienumerable && targetGeneric == ienumerable) + { + var sourceGenericType = sourceType.GenericTypeArguments[0]; + var targetGenericType = targetType.GenericTypeArguments[0]; + + ctor = GetCtor(sourceGenericType, targetGenericType); + map = GetMap(sourceGenericType, targetGenericType); + + if (ctor != null && map != null) + { + var sourceEnumerable = (IEnumerable)source; + var targetEnumerable = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericType)); + + foreach (var sourceItem in sourceEnumerable) + { + var targetItem = ctor(sourceItem); + map(sourceItem, targetItem); + targetEnumerable.Add(targetItem); + } + + return (TTarget)targetEnumerable; + } + + // fixme - temp + return AutoMapper.Mapper.Map(source); + } + } + + // fixme this is temp + //throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}."); + return AutoMapper.Mapper.Map(source); + } + + public TTarget Map(TSource source, TTarget target) + { + // fixme should we deal with enumerables? + var map = GetMap(source.GetType(), typeof(TTarget)); if (map == null) { // fixme this is temp //throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}."); - return AutoMapper.Mapper.Map(source); + return AutoMapper.Mapper.Map(source, target); } - return (TTarget) map(source); + map(source, target); + return target; } - private Func GetMap(Type sourceType, Type targetType) + private Func GetCtor(Type sourceType, Type targetType) + { + if (!_ctors.TryGetValue(sourceType, out var sourceCtor)) + { + var type = _maps.Keys.FirstOrDefault(x => x.IsAssignableFrom(sourceType)); + if (type == null) + return null; + sourceCtor = _ctors[sourceType] = _ctors[type]; + } + + return sourceCtor.TryGetValue(targetType, out var ctor) ? ctor : null; + } + + private Action GetMap(Type sourceType, Type targetType) { if (!_maps.TryGetValue(sourceType, out var sourceMap)) { @@ -58,41 +217,5 @@ namespace Umbraco.Core.Mapping return sourceMap.TryGetValue(targetType, out var map) ? map : null; } - - public TTarget Map(TSource source) - { - return AutoMapper.Mapper.Map(source); - - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); - - var map = GetMap(sourceType, targetType); - if (map != null) - return (TTarget) map(source); - - if (sourceType.IsGenericType && targetType.IsGenericType) - { - var sourceGeneric = sourceType.GetGenericTypeDefinition(); - var targetGeneric = targetType.GetGenericTypeDefinition(); - var ienumerable = typeof(IEnumerable<>); - - if (sourceGeneric == ienumerable && targetGeneric == ienumerable) - { - var sourceGenericType = sourceGeneric.GetGenericArguments()[0]; - var targetGenericType = targetGeneric.GetGenericArguments()[0]; - map = GetMap(sourceGenericType, targetGenericType); - // fixme - how can we enumerate, generically? - } - } - - // fixme this is temp - //throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}."); - return AutoMapper.Mapper.Map(source); - } - - public TTarget Map(TSource source, TTarget target) - { - return AutoMapper.Mapper.Map(source, target); // fixme what does this do exactly? - } } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs b/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs index 4035123d20..64d9c6f9a1 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityMapperProfile.cs @@ -21,18 +21,19 @@ namespace Umbraco.Core.Models.Identity public void SetMaps(Mapper mapper) { - mapper.SetMap(Map); - } - - public BackOfficeIdentityUser Map(IUser source) - { - var target = new BackOfficeIdentityUser(source.Id, source.Groups); - target.DisableChangeTracking(); - Map(source, target); - target.ResetDirtyProperties(true); - target.EnableChangeTracking(); - - return target; + mapper.SetMap( + source => + { + var target = new BackOfficeIdentityUser(source.Id, source.Groups); + target.DisableChangeTracking(); + return target; + }, + (source, target) => + { + Map(source, target); + target.ResetDirtyProperties(true); + target.EnableChangeTracking(); + }); } // Umbraco.Code.MapAll -Id -Groups -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs new file mode 100644 index 0000000000..4b2e0cfdec --- /dev/null +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core.Mapping; + +namespace Umbraco.Tests.Mapping +{ + [TestFixture] + public class MappingTests + { + [Test] + public void SimpleMap() + { + var profiles = new MapperProfileCollection(new IMapperProfile[] + { + new Profile1(), + }); + var mapper = new Mapper(profiles); + + var thing1 = new Thing1 { Value = "value" }; + var thing2 = mapper.Map(thing1); + + Assert.IsNotNull(thing2); + Assert.AreEqual("value", thing2.Value); + + thing2 = mapper.Map(thing1); + + Assert.IsNotNull(thing2); + Assert.AreEqual("value", thing2.Value); + + thing2 = new Thing2(); + mapper.Map(thing1, thing2); + Assert.AreEqual("value", thing2.Value); + } + + [Test] + public void EnumerableMap() + { + var profiles = new MapperProfileCollection(new IMapperProfile[] + { + new Profile1(), + }); + var mapper = new Mapper(profiles); + + var thing1A = new Thing1 { Value = "valueA" }; + var thing1B = new Thing1 { Value = "valueB" }; + var thing1 = new[] { thing1A, thing1B }; + var thing2 = mapper.Map, IEnumerable>(thing1).ToList(); + + Assert.IsNotNull(thing2); + Assert.AreEqual(2, thing2.Count); + Assert.AreEqual("valueA", thing2[0].Value); + Assert.AreEqual("valueB", thing2[1].Value); + + thing2 = mapper.Map>(thing1).ToList(); + + Assert.IsNotNull(thing2); + Assert.AreEqual(2, thing2.Count); + Assert.AreEqual("valueA", thing2[0].Value); + Assert.AreEqual("valueB", thing2[1].Value); + + // fixme is this a thing? + //thing2 = new List(); + //mapper.Map(thing1, thing2); + //Assert.AreEqual("value", thing2.Value); + } + + private class Thing1 + { + public string Value { get; set; } + } + + private class Thing2 + { + public string Value { get; set; } + } + + private class Profile1 : IMapperProfile + { + public void SetMaps(Mapper mapper) + { + mapper.SetMap(source => new Thing2(), (source, target) => Map(source, target)); + } + + private Thing2 Map(Thing1 source, Thing2 target) + { + target.Value = source.Value; + return target; + } + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e0622f0c27..706d81700b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -131,6 +131,7 @@ + diff --git a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs index b4fce14385..20967f5180 100644 --- a/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs +++ b/src/Umbraco.Web/Composing/CompositionExtensions/WebMappingProfiles.cs @@ -1,6 +1,7 @@ using AutoMapper; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; @@ -12,8 +13,14 @@ namespace Umbraco.Web.Composing.CompositionExtensions { public static Composition ComposeWebMappingProfiles(this Composition composition) { + // register the profiles + composition.WithCollectionBuilder() + .Append() + .Append() + .Append(); + //register the profiles - composition.Register(); + //composition.Register(); composition.Register(); composition.Register(); composition.Register(); @@ -26,8 +33,8 @@ namespace Umbraco.Web.Composing.CompositionExtensions composition.Register(); composition.Register(); composition.Register(); - composition.Register(); - composition.Register(); + //composition.Register(); + //composition.Register(); composition.Register(); composition.Register(); composition.Register(); diff --git a/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs index 34749c51c6..ffb16b0385 100644 --- a/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/AuditMapperProfile.cs @@ -1,20 +1,27 @@ -using AutoMapper; -using Umbraco.Core; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - internal class AuditMapperProfile : Profile + internal class AuditMapperProfile : IMapperProfile { - public AuditMapperProfile() + public void SetMaps(Mapper mapper) { - CreateMap() - .ForMember(log => log.UserAvatars, expression => expression.Ignore()) - .ForMember(log => log.UserName, expression => expression.Ignore()) - .ForMember(log => log.NodeId, expression => expression.MapFrom(item => item.Id)) - .ForMember(log => log.Timestamp, expression => expression.MapFrom(item => item.CreateDate)) - .ForMember(log => log.LogType, expression => expression.MapFrom(item => item.AuditType)); + mapper.SetMap(source => new AuditLog(), (source, target) => Map(source, target)); + } + + // Umbraco.Code.MapAll -UserAvatars -UserName + private AuditLog Map(IAuditItem source, AuditLog target) + { + target.UserId = source.UserId; + target.NodeId = source.Id; + target.Timestamp = source.CreateDate; + target.LogType = source.AuditType.ToString(); + target.EntityType = source.EntityType; + target.Comment = source.Comment; + target.Parameters = source.Parameters; + return target; } } } diff --git a/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs index 61dd8dcadc..91e3971d7c 100644 --- a/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/SectionMapperProfile.cs @@ -1,19 +1,48 @@ -using System.Collections.Generic; -using AutoMapper; +using Umbraco.Core.Manifest; +using Umbraco.Core.Mapping; using Umbraco.Core.Models.Sections; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Sections; namespace Umbraco.Web.Models.Mapping { - internal class SectionMapperProfile : Profile + internal class SectionMapperProfile : IMapperProfile { + private readonly ILocalizedTextService _textService; public SectionMapperProfile(ILocalizedTextService textService) { - CreateMap() - .ForMember(dest => dest.RoutePath, opt => opt.Ignore()) - .ForMember(dest => dest.Name, opt => opt.MapFrom(src => textService.Localize("sections/" + src.Alias, (IDictionary)null))) - .ReverseMap(); //backwards too! + _textService = textService; + } + + public void SetMaps(Mapper mapper) + { + mapper.SetMap(source => new Section(), Map); + + // this is for AutoMapper ReverseMap - but really? + mapper.SetMap(); + mapper.SetMap(); + mapper.SetMap(Map); + mapper.SetMap(); + mapper.SetMap(); + mapper.SetMap(); + mapper.SetMap(); + mapper.SetMap(); + mapper.SetMap(); + } + + // Umbraco.Code.MapAll -RoutePath + private void Map(ISection source, Section target) + { + target.Alias = source.Alias; + target.Name = _textService.Localize("sections/" + source.Alias); + } + + // Umbraco.Code.MapAll + private void Map(Section source, ManifestSection target) + { + target.Alias = source.Alias; + target.Name = source.Name; } } } diff --git a/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs index bcb6c2bb23..e788db8694 100644 --- a/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/TagMapperProfile.cs @@ -1,13 +1,22 @@ -using AutoMapper; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; namespace Umbraco.Web.Models.Mapping { - internal class TagMapperProfile : Profile + internal class TagMapperProfile : IMapperProfile { - public TagMapperProfile() + public void SetMaps(Mapper mapper) { - CreateMap(); + mapper.SetMap(source => new TagModel(), Map); + } + + // Umbraco.Code.MapAll + private void Map(ITag source, TagModel target) + { + target.Id = source.Id; + target.Text = source.Text; + target.Group = source.Group; + target.NodeCount = source.NodeCount; } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 09cc7d856a..3049a57f3d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -86,6 +86,9 @@ + + 1.0.0 +