Refactor more mappers

This commit is contained in:
Stephan
2019-03-20 19:09:23 +01:00
parent cd0338498f
commit a6dc5f0eac
9 changed files with 352 additions and 80 deletions

View File

@@ -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<Type, Dictionary<Type, Func<object, object>>> _maps = new Dictionary<Type, Dictionary<Type, Func<object, object>>>();
private readonly Dictionary<Type, Dictionary<Type, Func<object, object>>> _ctors = new Dictionary<Type, Dictionary<Type, Func<object, object>>>();
private readonly Dictionary<Type, Dictionary<Type, Action<object, object>>> _maps = new Dictionary<Type, Dictionary<Type, Action<object, object>>>();
public Mapper(MapperProfileCollection profiles)
{
@@ -19,15 +21,29 @@ namespace Umbraco.Core.Mapping
profile.SetMaps(this);
}
public void SetMap<TSource, TTarget>(Func<TSource, TTarget> map)
public void SetMap<TSource, TTarget>()
=> SetMap<TSource, TTarget>((source, target) => { });
public void SetMap<TSource, TTarget>(Action<TSource, TTarget> map)
=> SetMap(source => throw new NotSupportedException($"Don't know how to create {typeof(TTarget)} instances."), map);
public void SetMap<TSource, TTarget>(Func<TSource, TTarget> ctor)
=> SetMap(ctor, (source, target) => { });
public void SetMap<TSource, TTarget>(Func<TSource, TTarget> ctor, Action<TSource, TTarget> map)
{
var sourceType = typeof(TSource);
var targetType = typeof(TTarget);
if (!_maps.TryGetValue(sourceType, out var sourceMap))
sourceMap = _maps[sourceType] = new Dictionary<Type, Func<object, object>>();
if (!_ctors.TryGetValue(sourceType, out var sourceCtor))
sourceCtor = _ctors[sourceType] = new Dictionary<Type, Func<object, object>>();
sourceMap[targetType] = o => map((TSource)o);
sourceCtor[targetType] = source => ctor((TSource) source);
if (!_maps.TryGetValue(sourceType, out var sourceMap))
sourceMap = _maps[sourceType] = new Dictionary<Type, Action<object, object>>();
sourceMap[targetType] = (source, target) => map((TSource) source, (TTarget) target);
}
public TTarget Map<TTarget>(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<TTarget>(source);
}
}
// fixme this is temp
//throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}.");
return AutoMapper.Mapper.Map<TTarget>(source);
}
// TODO: when AutoMapper is completely gone these two methods can merge
public TTarget Map<TSource, TTarget>(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<TSource, TTarget>(source);
}
}
// fixme this is temp
//throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {targetType.FullName}.");
return AutoMapper.Mapper.Map<TSource, TTarget>(source);
}
public TTarget Map<TSource, TTarget>(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<TTarget>(source);
return AutoMapper.Mapper.Map(source, target);
}
return (TTarget) map(source);
map(source, target);
return target;
}
private Func<object, object> GetMap(Type sourceType, Type targetType)
private Func<object, object> 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<object, object> 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, TTarget>(TSource source)
{
return AutoMapper.Mapper.Map<TSource, TTarget>(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<TSource, TTarget>(source);
}
public TTarget Map<TSource, TTarget>(TSource source, TTarget target)
{
return AutoMapper.Mapper.Map(source, target); // fixme what does this do exactly?
}
}
}

View File

@@ -21,18 +21,19 @@ namespace Umbraco.Core.Models.Identity
public void SetMaps(Mapper mapper)
{
mapper.SetMap<IUser, BackOfficeIdentityUser>(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<IUser, BackOfficeIdentityUser>(
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

View File

@@ -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, Thing2>(thing1);
Assert.IsNotNull(thing2);
Assert.AreEqual("value", thing2.Value);
thing2 = mapper.Map<Thing2>(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>, IEnumerable<Thing2>>(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<IEnumerable<Thing2>>(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<Thing2>();
//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<Thing1, Thing2>(source => new Thing2(), (source, target) => Map(source, target));
}
private Thing2 Map(Thing1 source, Thing2 target)
{
target.Value = source.Value;
return target;
}
}
}
}

View File

@@ -131,6 +131,7 @@
<Compile Include="LegacyXmlPublishedCache\PreviewXmlDto.cs" />
<Compile Include="Logging\LogviewerTests.cs" />
<Compile Include="Manifest\ManifestContentAppTests.cs" />
<Compile Include="Mapping\MappingTests.cs" />
<Compile Include="Migrations\MigrationPlanTests.cs" />
<Compile Include="Migrations\MigrationTests.cs" />
<Compile Include="Models\ContentScheduleTests.cs" />

View File

@@ -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<MapperProfileCollectionBuilder>()
.Append<AuditMapperProfile>()
.Append<SectionMapperProfile>()
.Append<TagMapperProfile>();
//register the profiles
composition.Register<Profile, AuditMapperProfile>();
//composition.Register<Profile, AuditMapperProfile>();
composition.Register<Profile, CodeFileMapperProfile>();
composition.Register<Profile, ContentMapperProfile>();
composition.Register<Profile, ContentPropertyMapperProfile>();
@@ -26,8 +33,8 @@ namespace Umbraco.Web.Composing.CompositionExtensions
composition.Register<Profile, MemberMapperProfile>();
composition.Register<Profile, RedirectUrlMapperProfile>();
composition.Register<Profile, RelationMapperProfile>();
composition.Register<Profile, SectionMapperProfile>();
composition.Register<Profile, TagMapperProfile>();
//composition.Register<Profile, SectionMapperProfile>();
//composition.Register<Profile, TagMapperProfile>();
composition.Register<Profile, TemplateMapperProfile>();
composition.Register<Profile, UserMapperProfile>();
composition.Register<Profile, LanguageMapperProfile>();

View File

@@ -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<IAuditItem, AuditLog>()
.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<IAuditItem, AuditLog>(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;
}
}
}

View File

@@ -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<ISection, Section>()
.ForMember(dest => dest.RoutePath, opt => opt.Ignore())
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => textService.Localize("sections/" + src.Alias, (IDictionary<string, string>)null)))
.ReverseMap(); //backwards too!
_textService = textService;
}
public void SetMaps(Mapper mapper)
{
mapper.SetMap<ISection, Section>(source => new Section(), Map);
// this is for AutoMapper ReverseMap - but really?
mapper.SetMap<Section, ContentSection>();
mapper.SetMap<Section, ContentSection>();
mapper.SetMap<Section, ManifestSection>(Map);
mapper.SetMap<Section, MediaSection>();
mapper.SetMap<Section, MembersSection>();
mapper.SetMap<Section, PackagesSection>();
mapper.SetMap<Section, SettingsSection>();
mapper.SetMap<Section, TranslationSection>();
mapper.SetMap<Section, UsersSection>();
}
// 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;
}
}
}

View File

@@ -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<ITag, TagModel>();
mapper.SetMap<ITag, TagModel>(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;
}
}
}

View File

@@ -86,6 +86,9 @@
<PackageReference Include="NPoco" Version="3.9.4" />
<PackageReference Include="Semver" Version="2.0.4" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />
<PackageReference Include="Umbraco.Code">
<Version>1.0.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">