using System.Collections.Concurrent; namespace zero.Mapper; public class ZeroMapper : IZeroMapper { /// /// Concurrent cache for all ctor definitions /// protected ConcurrentDictionary>> MapDefinitions { get; private set; } = new(); /// /// Concurrent cache for all constructor definitions /// protected ConcurrentDictionary>> ConstructorDefinitions { get; private set; } = new(); public ZeroMapper(IEnumerable profiles) { foreach (IMapperProfile profile in profiles) { profile.Configure(this); } } /// public void Define(Func ctor, Action map) { Type sourceType = typeof(TSource); Type destinationType = typeof(TDestination); var sourceMaps = MapDefinitions.GetOrAdd(sourceType, _ => new()); var sourceCtors = ConstructorDefinitions.GetOrAdd(sourceType, _ => new()); sourceCtors[destinationType] = (source, ctx) => ctor((TSource)source, ctx); sourceMaps[destinationType] = (source, destination, ctx) => map((TSource)source, (TDestination)destination, ctx); } /// public void Override(Func ctor, Action> map) { Type sourceType = typeof(TSource); Type destinationType = typeof(TDestination); var sourceMaps = MapDefinitions.GetValueOrDefault(sourceType); var sourceCtors = ConstructorDefinitions.GetValueOrDefault(sourceType); if (sourceMaps == null || sourceCtors == null || !sourceMaps.ContainsKey(destinationType)) { return; } var baseMap = sourceMaps[destinationType]; sourceCtors[destinationType] = (source, ctx) => ctor((TSource)source, ctx); sourceMaps[destinationType] = (source, destination, ctx) => map((TSource)source, (TDestination)destination, ctx, (source, destination, ctx) => baseMap(source, destination, ctx)); } /// public TDestination Map(object source, Type sourceType, TDestination destination = default) { if (source == null) { return default; } Type destinationType = typeof(TDestination); if (destinationType == source.GetType()) { return (TDestination)source; } ZeroMapperContext mapperContext = new(this); var constructor = GetConstructor(sourceType, destinationType); var map = GetMap(sourceType, destinationType); if (constructor != null && map != null) { destination ??= (TDestination)constructor(source, mapperContext); map(source, destination, mapperContext); return destination; } // TODO enumerables throw new InvalidOperationException($"Don't know how to map {sourceType.FullName} to {destinationType.FullName}."); } protected virtual Func GetConstructor(Type sourceType, Type targetType) { if (ConstructorDefinitions.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) { return ctor; } // we *may* run this more than once but it does not matter ctor = null; foreach (var (stype, sctors) in ConstructorDefinitions) { if (!stype.IsAssignableFrom(sourceType)) continue; if (!sctors.TryGetValue(targetType, out ctor)) continue; sourceCtor = sctors; break; } if (ctor == null) { return null; } ConstructorDefinitions.AddOrUpdate(sourceType, sourceCtor, (k, v) => { // Add missing constructors foreach (var c in sourceCtor) { if (!v.ContainsKey(c.Key)) { v.Add(c.Key, c.Value); } } return v; }); return ctor; } protected virtual Action GetMap(Type sourceType, Type targetType) { if (MapDefinitions.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) { return map; } // we *may* run this more than once but it does not matter map = null; foreach (var (stype, smap) in MapDefinitions) { if (!stype.IsAssignableFrom(sourceType)) continue; // TODO: consider looking for assignable types for target too? if (!smap.TryGetValue(targetType, out map)) continue; sourceMap = smap; break; } if (map == null) return null; if (MapDefinitions.ContainsKey(sourceType)) { foreach (var m in sourceMap) { if (!MapDefinitions[sourceType].TryGetValue(m.Key, out _)) { MapDefinitions[sourceType].Add(m.Key, m.Value); } } } else { MapDefinitions[sourceType] = sourceMap; } return map; } } public interface IZeroMapper { /// /// Register a new mapping /// void Define(Func ctor, Action map); /// void Override(Func ctor, Action> map); /// /// Map a source type to the destination type /// TDestination Map(object source, Type sourceType, TDestination destination = default); }