diff --git a/GitVersion.yml b/GitVersion.yml index 89b0a62..d9fd3d8 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -next-version: 1.0.0 +next-version: 1.2.0 tag-prefix: '[vV]' mode: ContinuousDeployment branches: diff --git a/README.md b/README.md index c4528d0..a749c32 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ninja TurboMapper v1.0.0 +# ninja TurboMapper v1.2.0 [![NuGet version](https://badge.fury.io/nu/TurboMapper.svg)](https://badge.fury.io/nu/TurboMapper) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/TurboMapper/blob/master/LICENSE.md) [![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/TurboMapper?logo=github&sort=semver)](https://github.com/CodeShayk/TurboMapper/releases/latest) [![master-build](https://github.com/CodeShayk/TurboMapper/actions/workflows/Master-Build.yml/badge.svg)](https://github.com/CodeShayk/TurboMapper/actions/workflows/Master-Build.yml) @@ -10,24 +10,65 @@ ## Introduction ### What is TurboMapper? `TurboMapper` is a lightweight, high-performance object mapper for .NET that provides both shallow and deep mapping capabilities. It serves as a free alternative to AutoMapper with a simple, intuitive API. -## Getting Started? + +## Getting Started ### i. Installation Install the latest version of TurboMapper nuget package with command below. ``` NuGet\Install-Package TurboMapper ``` -### ii. Developer Guide + +### ii. Quick Start Example +```csharp +using TurboMapper; +using Microsoft.Extensions.DependencyInjection; + +// Setup +var services = new ServiceCollection(); +services.AddTurboMapper(); +var serviceProvider = services.BuildServiceProvider(); +var mapper = serviceProvider.GetService(); + +// Define models +public class Source +{ + public string Name { get; set; } + public int Age { get; set; } +} + +public class Target +{ + public string Name { get; set; } + public int Age { get; set; } +} + +// Map single object +var source = new Source { Name = "John Doe", Age = 30 }; +var target = mapper.Map(source); + +// Map collections +var sources = new List +{ + new Source { Name = "Alice", Age = 25 }, + new Source { Name = "Bob", Age = 32 } +}; + +// Map to IEnumerable +IEnumerable targets = mapper.Map(sources); +``` + +### iii. Developer Guide This comprehensive guide provides detailed information on TurboMapper, covering everything from basic concepts to advanced implementations and troubleshooting guidelines. Please click on [Developer Guide](https://github.com/CodeShayk/TurboMapper/wiki) for complete details. ## Release Roadmap -This section provides the summary of planned releases with key details about each release. +This section provides the summary of planned releases with key details about each release. | Release Version | Release Date | Key Features | Backward Compatibility | Primary Focus | |----------------|--------------|--------------|----------------------|---------------| -| 1.2.0 | October 2025 | Performance improvements (2x+ speed), collection mapping, custom type converters, conditional mapping, transformation functions, configuration validation, improved error messages | ✅ Fully backward compatible | Core improvements, mapping features, custom conversions | +| 1.2.0 | October 2025 | Performance improvements (2x+ speed), enhanced collection mapping API, custom type converters, conditional mapping, transformation functions, configuration validation, improved error messages | ✅ Fully backward compatible | Core improvements, mapping features, custom conversions | | 1.4.0 | Jan 2026 | Complex nested mapping, circular reference handling, performance diagnostics, generic collection interfaces, interface-to-concrete mapping, dictionary mapping, .NET Standard compatibility | ✅ Fully backward compatible | Advanced mapping, type features, enhanced conversions | | 2.1.0 | Mid 2026 | Pre-compiled mappings, reverse mapping, async transformations, async collection processing, LINQ expressions, projection support, detailed tracing | ❌ Contains breaking changes (new async methods in IMapper) | Next-gen features, async operations, data access integration | diff --git a/src/TurboMapper/IMapper.cs b/src/TurboMapper/IMapper.cs index 510fc56..3bbb173 100644 --- a/src/TurboMapper/IMapper.cs +++ b/src/TurboMapper/IMapper.cs @@ -1,7 +1,13 @@ +using System.Collections.Generic; + namespace TurboMapper { public interface IMapper { TTarget Map(TSource source); + + IEnumerable Map(IEnumerable source); + + ValidationResult ValidateMapping(); } } \ No newline at end of file diff --git a/src/TurboMapper/IMappingExpression.cs b/src/TurboMapper/IMappingExpression.cs index ce0da01..947b0e4 100644 --- a/src/TurboMapper/IMappingExpression.cs +++ b/src/TurboMapper/IMappingExpression.cs @@ -6,5 +6,11 @@ namespace TurboMapper public interface IMappingExpression { IMappingExpression ForMember(Expression> targetMember, Expression> sourceMember); + + IMappingExpression Ignore(Expression> targetMember); + + IMappingExpression When(Expression> targetMember, Func condition); + + IMappingExpression MapWith(Expression> targetMember, Func transformFunction); } } \ No newline at end of file diff --git a/src/TurboMapper/IObjectMap.cs b/src/TurboMapper/IObjectMap.cs index 0f845f2..3fe6038 100644 --- a/src/TurboMapper/IObjectMap.cs +++ b/src/TurboMapper/IObjectMap.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace TurboMapper @@ -7,5 +8,7 @@ internal interface IObjectMap void CreateMap(List mappings = null); void CreateMap(List mappings, bool enableDefaultMapping); + + void RegisterConverter(Func converter); } } \ No newline at end of file diff --git a/src/TurboMapper/Impl/Mapper.cs b/src/TurboMapper/Impl/Mapper.cs index 7cdc14f..38384df 100644 --- a/src/TurboMapper/Impl/Mapper.cs +++ b/src/TurboMapper/Impl/Mapper.cs @@ -22,10 +22,20 @@ public MapperConfiguration(List mappings, bool enableDefaultMap internal class Mapper : IMapper, IObjectMap { private readonly Dictionary> _configurations; + private readonly Dictionary _propertyCache; + private readonly Dictionary _propertyPathCache; + private readonly Dictionary> _factoryCache; + private readonly Dictionary> _getterCache; + private readonly Dictionary> _setterCache; public Mapper() { _configurations = new Dictionary>(); + _propertyCache = new Dictionary(); + _propertyPathCache = new Dictionary(); + _factoryCache = new Dictionary>(); + _getterCache = new Dictionary>(); + _setterCache = new Dictionary>(); } public void CreateMap(List mappings = null) @@ -58,13 +68,11 @@ public TTarget Map(TSource source) var sourceType = typeof(TSource); var targetType = typeof(TTarget); - var target = Activator.CreateInstance(); + var target = (TTarget)CreateInstance(targetType); // Check for custom mapping configuration - if (_configurations.ContainsKey(sourceType) && - _configurations[sourceType].ContainsKey(targetType)) + if (TryGetConfiguration(sourceType, targetType, out var config)) { - var config = _configurations[sourceType][targetType]; if (config.EnableDefaultMapping) ApplyCustomMappings(source, target, config.Mappings); else @@ -77,16 +85,44 @@ public TTarget Map(TSource source) return target; } + private bool TryGetConfiguration(Type sourceType, Type targetType, out MapperConfiguration config) + { + config = null; + + if (_configurations.TryGetValue(sourceType, out var targetConfigs) && + targetConfigs.TryGetValue(targetType, out config)) + { + return true; + } + + return false; + } + internal void ApplyCustomMappings( TSource source, TTarget target, List mappings) { - // First, apply all custom mappings + // First, apply all non-ignored custom mappings that meet conditions foreach (var mapping in mappings) { - var sourceValue = GetNestedValue(source, mapping.SourcePropertyPath); - SetNestedValue(target, mapping.TargetPropertyPath, sourceValue); + if (!mapping.IsIgnored && (mapping.Condition == null || mapping.Condition(source))) + { + var sourceValue = GetNestedValue(source, mapping.SourcePropertyPath); + + // Apply transformation if available + if (mapping.TransformFunction != null && sourceValue != null) + { + // Use reflection to call the transformation function + var transformFunc = (Delegate)mapping.TransformFunction; + var transformedValue = transformFunc.DynamicInvoke(sourceValue); + SetNestedValue(target, mapping.TargetPropertyPath, transformedValue); + } + else + { + SetNestedValue(target, mapping.TargetPropertyPath, sourceValue); + } + } } // Then apply default name-based mapping for unmapped properties @@ -98,21 +134,42 @@ internal void ApplyCustomMappingsWithDefaultDisabled( TTarget target, List mappings) { - // Apply only custom mappings, no default mappings + // Apply only non-ignored custom mappings that meet conditions, no default mappings foreach (var mapping in mappings) { - var sourceValue = GetNestedValue(source, mapping.SourcePropertyPath); - SetNestedValue(target, mapping.TargetPropertyPath, sourceValue); + if (!mapping.IsIgnored && (mapping.Condition == null || mapping.Condition(source))) + { + var sourceValue = GetNestedValue(source, mapping.SourcePropertyPath); + + // Apply transformation if available + if (mapping.TransformFunction != null && sourceValue != null) + { + // Use reflection to call the transformation function + var transformFunc = (Delegate)mapping.TransformFunction; + var transformedValue = transformFunc.DynamicInvoke(sourceValue); + SetNestedValue(target, mapping.TargetPropertyPath, transformedValue); + } + else + { + SetNestedValue(target, mapping.TargetPropertyPath, sourceValue); + } + } } } + public void RegisterConverter(Func converter) + { + var key = $"{typeof(TSource).FullName}_{typeof(TDestination).FullName}"; + _converters[key] = converter; + } + private void ApplyDefaultNameBasedMapping( TSource source, TTarget target, List customMappings) { - var sourceProps = typeof(TSource).GetProperties(); - var targetProps = typeof(TTarget).GetProperties(); + var sourceProps = GetTypeProperties(typeof(TSource)); + var targetProps = GetTypeProperties(typeof(TTarget)); foreach (var sourceProp in sourceProps) { @@ -122,55 +179,110 @@ private void ApplyDefaultNameBasedMapping( p.Name == sourceProp.Name && p.CanWrite); - if (targetProp != null) + if (targetProp != null && !IsTargetedInCustomMappings(targetProp.Name, customMappings)) { - // Check if this target property is already targeted by any custom mapping - var isTargeted = customMappings.Exists(m => - m.TargetPropertyPath.Split('.').Last() == targetProp.Name); + ProcessPropertyMapping(source, target, sourceProp, targetProp); + } + } + } - if (!isTargeted) - { - var sourceValue = sourceProp.GetValue(source); + private bool IsTargetedInCustomMappings(string targetPropertyName, List customMappings) + { + return customMappings.Exists(m => + m.TargetPropertyPath.Split('.').Last() == targetPropertyName && !m.IsIgnored); + } + + private bool IsIgnoredInCustomMappings(string targetPropertyName, List customMappings) + { + return customMappings.Exists(m => + m.TargetPropertyPath.Split('.').Last() == targetPropertyName && m.IsIgnored); + } - if (IsComplexType(sourceProp.PropertyType) && IsComplexType(targetProp.PropertyType)) + // Custom converter system + private readonly Dictionary _converters = new Dictionary(); + + public ValidationResult ValidateMapping() + { + var errors = new List(); + var sourceType = typeof(TSource); + var targetType = typeof(TTarget); + + // Check if mapping configuration exists + if (TryGetConfiguration(sourceType, targetType, out var config)) + { + if (config.Mappings != null) + { + foreach (var mapping in config.Mappings) + { + // Validate source property exists + if (!mapping.IsIgnored && !mapping.IsNested && !string.IsNullOrEmpty(mapping.SourcePropertyPath)) { - // Handle nested object mapping - if (sourceValue != null) - { - var nestedTargetValue = targetProp.GetValue(target); - if (nestedTargetValue == null) - { - nestedTargetValue = Activator.CreateInstance(targetProp.PropertyType); - targetProp.SetValue(target, nestedTargetValue); - } - - var nestedSourceValue = sourceValue; - // Use reflection to call the right generic method for nested mapping - var genericMethod = typeof(Mapper).GetMethod(nameof(ApplyNameBasedMapping), - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var specificMethod = genericMethod.MakeGenericMethod(sourceProp.PropertyType, targetProp.PropertyType); - specificMethod.Invoke(this, new object[] { nestedSourceValue, nestedTargetValue }); - } - else + if (!PropertyExists(sourceType, mapping.SourcePropertyPath)) { - targetProp.SetValue(target, null); + errors.Add($"Source property '{mapping.SourcePropertyPath}' does not exist on type '{sourceType.Name}'"); } } - else + + // Validate target property exists + if (!mapping.IsIgnored && !mapping.IsNested && !string.IsNullOrEmpty(mapping.TargetPropertyPath)) { - // Handle simple types or type conversion - var convertedValue = ConvertValue(sourceValue, targetProp.PropertyType); - targetProp.SetValue(target, convertedValue); + if (!PropertyExists(targetType, mapping.TargetPropertyPath)) + { + errors.Add($"Target property '{mapping.TargetPropertyPath}' does not exist on type '{targetType.Name}'"); + } } } } } + + var isValid = errors.Count == 0; + return new ValidationResult(isValid, errors); + } + + private bool PropertyExists(Type type, string propertyPath) + { + var properties = propertyPath.Split('.'); + Type currentType = type; + + foreach (var prop in properties) + { + var propertyInfo = currentType.GetProperty(prop); + if (propertyInfo == null) + return false; + + currentType = propertyInfo.PropertyType; + } + + return true; + } + + private bool TryConvertWithCustomConverter(object value, Type targetType, out object result) + { + result = null; + + if (value == null) + return false; + + var key = $"{value.GetType().FullName}_{targetType.FullName}"; + + if (_converters.TryGetValue(key, out var converter)) + { + var funcType = typeof(Func<,>).MakeGenericType(value.GetType(), targetType); + if (converter.GetType() == funcType || converter.GetType().IsSubclassOf(typeof(MulticastDelegate))) + { + // Use reflection to invoke the appropriate converter function + result = converter.DynamicInvoke(value); + return true; + } + } + + return false; } internal void ApplyNameBasedMapping(TSource source, TTarget target) { - var sourceProps = typeof(TSource).GetProperties(); - var targetProps = typeof(TTarget).GetProperties(); + var sourceProps = GetTypeProperties(typeof(TSource)); + var targetProps = GetTypeProperties(typeof(TTarget)); foreach (var sourceProp in sourceProps) { @@ -179,39 +291,85 @@ internal void ApplyNameBasedMapping(TSource source, TTarget ta if (targetProp != null) { - var sourceValue = sourceProp.GetValue(source); + ProcessPropertyMapping(source, target, sourceProp, targetProp); + } + } + } - if (IsComplexType(sourceProp.PropertyType) && IsComplexType(targetProp.PropertyType)) - { - // Handle nested object mapping - if (sourceValue != null) - { - var nestedTargetValue = targetProp.GetValue(target); - if (nestedTargetValue == null) - { - nestedTargetValue = Activator.CreateInstance(targetProp.PropertyType); - targetProp.SetValue(target, nestedTargetValue); - } + private void ProcessPropertyMapping( + TSource source, + TTarget target, + System.Reflection.PropertyInfo sourceProp, + System.Reflection.PropertyInfo targetProp) + { + var sourceGetter = GetOrCreateGetter(typeof(TSource), sourceProp.Name); + var targetSetter = GetOrCreateSetter(typeof(TTarget), targetProp.Name); - // Recursively map the nested object properties using reflection - var genericMethod = typeof(Mapper).GetMethod(nameof(ApplyNameBasedMapping), - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var specificMethod = genericMethod.MakeGenericMethod(sourceProp.PropertyType, targetProp.PropertyType); - specificMethod.Invoke(this, new object[] { sourceValue, nestedTargetValue }); - } - else - { - targetProp.SetValue(target, null); - } - } - else - { - // Handle simple types or type conversion - var convertedValue = ConvertValue(sourceValue, targetProp.PropertyType); - targetProp.SetValue(target, convertedValue); - } - } + var sourceValue = sourceGetter?.Invoke(source); + + if (IsComplexType(sourceProp.PropertyType) && IsComplexType(targetProp.PropertyType)) + { + HandleComplexTypeMapping(sourceValue, target, targetProp, targetSetter); + } + else + { + HandleSimpleTypeMapping(sourceValue, target, targetProp, targetSetter); + } + } + + private void HandleComplexTypeMapping( + object sourceValue, + TTarget target, + System.Reflection.PropertyInfo targetProp, + Action targetSetter) + { + if (sourceValue != null) + { + var nestedTargetValue = GetOrCreateNestedObject(target, targetProp); + // Recursively map the nested object properties using reflection + var genericMethod = typeof(Mapper).GetMethod(nameof(ApplyNameBasedMapping), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var specificMethod = genericMethod.MakeGenericMethod(sourceValue.GetType(), targetProp.PropertyType); + specificMethod.Invoke(this, new object[] { sourceValue, nestedTargetValue }); + } + else + { + targetSetter?.Invoke(target, null); + } + } + + private void HandleSimpleTypeMapping( + object sourceValue, + TTarget target, + System.Reflection.PropertyInfo targetProp, + Action targetSetter) + { + try + { + var convertedValue = ConvertValue(sourceValue, targetProp.PropertyType); + targetSetter?.Invoke(target, convertedValue); + } + catch + { + // If conversion fails, skip the property mapping (leave target property at its default value) + // This allows for graceful handling of incompatible types + } + } + + private object GetOrCreateNestedObject( + TTarget target, + System.Reflection.PropertyInfo targetProp) + { + var targetGetter = GetOrCreateGetter(typeof(TTarget), targetProp.Name); + var targetSetter = GetOrCreateSetter(typeof(TTarget), targetProp.Name); + + var nestedTargetValue = targetGetter?.Invoke(target); + if (nestedTargetValue == null) + { + nestedTargetValue = CreateInstance(targetProp.PropertyType); + targetSetter?.Invoke(target, nestedTargetValue); } + return nestedTargetValue; } private object GetNestedValue(object obj, string propertyPath) @@ -224,11 +382,13 @@ private object GetNestedValue(object obj, string propertyPath) if (currentObject == null) return null; - var propInfo = currentObject.GetType().GetProperty(property); - if (propInfo == null) + var type = currentObject.GetType(); + var getter = GetOrCreateGetter(type, property); + + if (getter == null) return null; - currentObject = propInfo.GetValue(currentObject); + currentObject = getter(currentObject); } return currentObject; @@ -241,25 +401,33 @@ private void SetNestedValue(object obj, string propertyPath, object value) for (var i = 0; i < properties.Length - 1; i++) { - var propInfo = currentObject.GetType().GetProperty(properties[i]); - if (propInfo == null) + var type = currentObject.GetType(); + var getter = GetOrCreateGetter(type, properties[i]); + + if (getter == null) return; - var nestedValue = propInfo.GetValue(currentObject); + var nestedValue = getter(currentObject); if (nestedValue == null) { - nestedValue = Activator.CreateInstance(propInfo.PropertyType); - propInfo.SetValue(currentObject, nestedValue); + nestedValue = CreateInstance(type.GetProperty(properties[i]).PropertyType); + var innerSetter = GetOrCreateSetter(type, properties[i]); // Renamed from 'setter' to 'innerSetter' + if (innerSetter != null) + innerSetter(currentObject, nestedValue); } currentObject = nestedValue; } - var lastPropInfo = currentObject.GetType().GetProperty(properties[properties.Length - 1]); - if (lastPropInfo != null) + var lastType = currentObject.GetType(); + var lastPropertyName = properties[properties.Length - 1]; + var setter = GetOrCreateSetter(lastType, lastPropertyName); + + if (setter != null) { + var lastPropInfo = lastType.GetProperty(lastPropertyName); var convertedValue = ConvertValue(value, lastPropInfo.PropertyType); - lastPropInfo.SetValue(currentObject, convertedValue); + setter(currentObject, convertedValue); } } @@ -276,42 +444,164 @@ private bool IsComplexType(Type type) return true; } + private bool IsNullableType(Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + private object ConvertValue(object value, Type targetType) { if (value == null) return null; + + // First, try custom converters + if (TryConvertWithCustomConverter(value, targetType, out var customResult)) + { + return customResult; + } + + // Handle nullable types + if (IsNullableType(targetType)) + { + if (value == null) + return null; + + var underlyingType = Nullable.GetUnderlyingType(targetType); + var convertedValue = ConvertValue(value, underlyingType); + return convertedValue; + } + + // If source is nullable and target is not, extract the value + if (IsNullableType(value.GetType())) + { + var underlyingType = Nullable.GetUnderlyingType(value.GetType()); + if (underlyingType != null) + { + var property = value.GetType().GetProperty("Value"); + if (property != null) + { + value = property.GetValue(value); + } + } + } + if (targetType.IsAssignableFrom(value.GetType())) return value; if (targetType.IsEnum) - return Enum.Parse(targetType, value.ToString(), ignoreCase: true); + { + try + { + return Enum.Parse(targetType, value.ToString(), ignoreCase: true); + } + catch (ArgumentException ex) // This catches invalid enum values + { + throw ex; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to convert '{value}' to enum type '{targetType.Name}': {ex.Message}", ex); + } + } if (targetType == typeof(string)) - return value.ToString(); + { + return value?.ToString(); + } if (targetType == typeof(Guid)) - return Guid.Parse(value.ToString()); + { + try + { + return Guid.Parse(value.ToString()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to convert '{value}' to Guid: {ex.Message}", ex); + } + } if (targetType.IsValueType) { try { - if (targetType == typeof(int) && value is double doubleValue) - return (int)doubleValue; // Explicit truncation for double to int - else if (targetType == typeof(int) && value is float floatValue) - return (int)floatValue; // Explicit truncation for float to int + // Enhanced type conversion with more specific handling + if (targetType == typeof(int)) + { + if (value is double doubleValue) + return (int)doubleValue; // Explicit truncation for double to int + else if (value is float floatValue) + return (int)floatValue; // Explicit truncation for float to int + else if (value is decimal decimalValue) + return (int)decimalValue; + else + return Convert.ToInt32(value); + } + else if (targetType == typeof(long)) + { + return Convert.ToInt64(value); + } + else if (targetType == typeof(float)) + { + if (value is double doubleValue) + return (float)doubleValue; + else if (value is decimal decimalValue) + return (float)decimalValue; + else + return Convert.ToSingle(value); + } + else if (targetType == typeof(double)) + { + if (value is float floatValue) + return (double)floatValue; + else if (value is decimal decimalValue) + return (double)decimalValue; + else + return Convert.ToDouble(value); + } + else if (targetType == typeof(decimal)) + { + if (value is double doubleValue) + return (decimal)doubleValue; + else if (value is float floatValue) + return (decimal)floatValue; + else + return Convert.ToDecimal(value); + } + else if (targetType == typeof(DateTime)) + { + if (value is string stringValue) + return DateTime.Parse(stringValue); + else if (value is long longValue) // Assuming timestamp + return DateTime.FromBinary(longValue); + else + return (DateTime)Convert.ChangeType(value, targetType); + } + else if (targetType == typeof(TimeSpan)) + { + if (value is string stringValue) + return TimeSpan.Parse(stringValue); + else if (value is long ticksValue) + return TimeSpan.FromTicks(ticksValue); + else + return (TimeSpan)Convert.ChangeType(value, targetType); + } else + { return Convert.ChangeType(value, targetType); + } } - catch (FormatException) + catch (FormatException ex) { - // If conversion fails, return default value for the target type - return Activator.CreateInstance(targetType); + throw new InvalidOperationException($"Failed to convert '{value}' (type: {value.GetType().Name}) to '{targetType.Name}': Format exception - {ex.Message}", ex); } - catch (InvalidCastException) + catch (InvalidCastException ex) { - // If conversion fails, return default value for the target type - return Activator.CreateInstance(targetType); + throw new InvalidOperationException($"Failed to convert '{value}' (type: {value.GetType().Name}) to '{targetType.Name}': Invalid cast - {ex.Message}", ex); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to convert '{value}' (type: {value.GetType().Name}) to '{targetType.Name}': {ex.Message}", ex); } } @@ -332,10 +622,9 @@ private object ConvertValue(object value, Type targetType) return specificMapMethod.Invoke(this, new object[] { value }); } } - catch + catch (Exception ex) { - // If mapping fails, return the original value - return value; + throw new InvalidOperationException($"Failed to map complex type '{value.GetType().Name}' to '{targetType.Name}': {ex.Message}", ex); } } @@ -357,5 +646,117 @@ private object Map(object source, Type targetType) var specificMapMethod = genericMapMethod.MakeGenericMethod(sourceType, targetType); return specificMapMethod.Invoke(this, new object[] { source }); } + + private System.Reflection.PropertyInfo[] GetTypeProperties(Type type) + { + if (_propertyCache.TryGetValue(type, out var properties)) + { + return properties; + } + + properties = type.GetProperties(); + _propertyCache[type] = properties; + return properties; + } + + private Func GetOrCreateFactory(Type type) + { + if (_factoryCache.TryGetValue(type, out var factory)) + { + return factory; + } + + // Create factory using reflection + var constructor = type.GetConstructor(Type.EmptyTypes); + if (constructor == null) + { + // If no parameterless constructor, return null or throw exception + throw new InvalidOperationException($"Type {type} does not have a parameterless constructor"); + } + + // Create factory delegate using expression trees for better performance + var newExpr = System.Linq.Expressions.Expression.New(constructor); + var lambda = System.Linq.Expressions.Expression.Lambda>(newExpr); + factory = lambda.Compile(); + + _factoryCache[type] = factory; + return factory; + } + + private object CreateInstance(Type type) + { + var factory = GetOrCreateFactory(type); + return factory(); + } + + private Func GetOrCreateGetter(Type objType, string propertyName) + { + var cacheKey = $"{objType.FullName}.{propertyName}"; + + if (_getterCache.TryGetValue(cacheKey, out var getter)) + { + return getter; + } + + var propertyInfo = objType.GetProperty(propertyName); + if (propertyInfo == null) + { + return null; + } + + var param = System.Linq.Expressions.Expression.Parameter(typeof(object), "obj"); + var convertParam = System.Linq.Expressions.Expression.Convert(param, objType); + var property = System.Linq.Expressions.Expression.Property(convertParam, propertyInfo); + var convertResult = System.Linq.Expressions.Expression.Convert(property, typeof(object)); + + var lambda = System.Linq.Expressions.Expression.Lambda>(convertResult, param); + getter = lambda.Compile(); + + _getterCache[cacheKey] = getter; + return getter; + } + + private Action GetOrCreateSetter(Type objType, string propertyName) + { + var cacheKey = $"{objType.FullName}.{propertyName}"; + + if (_setterCache.TryGetValue(cacheKey, out var setter)) + { + return setter; + } + + var propertyInfo = objType.GetProperty(propertyName); + if (propertyInfo == null || !propertyInfo.CanWrite) + { + return null; + } + + var objParam = System.Linq.Expressions.Expression.Parameter(typeof(object), "obj"); + var valueParam = System.Linq.Expressions.Expression.Parameter(typeof(object), "value"); + + var convertObj = System.Linq.Expressions.Expression.Convert(objParam, objType); + var convertValue = System.Linq.Expressions.Expression.Convert(valueParam, propertyInfo.PropertyType); + var property = System.Linq.Expressions.Expression.Property(convertObj, propertyInfo); + var assign = System.Linq.Expressions.Expression.Assign(property, convertValue); + + var lambda = System.Linq.Expressions.Expression.Lambda>(assign, objParam, valueParam); + setter = lambda.Compile(); + + _setterCache[cacheKey] = setter; + return setter; + } + + public IEnumerable Map(IEnumerable source) + { + if (source == null) + return null; + + var result = new List(); + foreach (var item in source) + { + result.Add(Map(item)); + } + return result; + } } } \ No newline at end of file diff --git a/src/TurboMapper/MappingExpression.cs b/src/TurboMapper/MappingExpression.cs index ee10b99..f6dc950 100644 --- a/src/TurboMapper/MappingExpression.cs +++ b/src/TurboMapper/MappingExpression.cs @@ -11,8 +11,8 @@ public IMappingExpression ForMember( Expression> targetMember, Expression> sourceMember) { - var targetPath = GetMemberPath(targetMember); - var sourcePath = GetMemberPath(sourceMember); + var targetPath = GetMemberPathForTarget(targetMember); + var sourcePath = GetMemberPathForSource(sourceMember); Mappings.Add(new PropertyMapping { @@ -25,22 +25,17 @@ public IMappingExpression ForMember( return this; } - private string GetMemberPath(Expression> expression) + private string GetMemberPathForTarget(Expression> expression) { - var path = new System.Collections.Generic.List(); - var memberExpression = expression.Body as MemberExpression; - - while (memberExpression != null) - { - path.Add(memberExpression.Member.Name); - memberExpression = memberExpression.Expression as MemberExpression; - } + return GetMemberPathInternal(expression); + } - path.Reverse(); - return string.Join(".", path); + private string GetMemberPathForSource(Expression> expression) + { + return GetMemberPathInternal(expression); } - private string GetMemberPath(Expression> expression) + private static string GetMemberPathInternal(Expression> expression) { var path = new System.Collections.Generic.List(); var memberExpression = expression.Body as MemberExpression; @@ -60,5 +55,59 @@ private string GetLastPropertyName(string path) var parts = path.Split('.'); return parts[parts.Length - 1]; } + + public IMappingExpression Ignore(Expression> targetMember) + { + var targetPath = GetMemberPathForTarget(targetMember); + var targetProperty = GetLastPropertyName(targetPath); + + // Add an ignored property mapping + Mappings.Add(new PropertyMapping + { + TargetProperty = targetProperty, + TargetPropertyPath = targetPath, + IsIgnored = true + }); + + return this; + } + + public IMappingExpression When(Expression> targetMember, Func condition) + { + var targetPath = GetMemberPathForTarget(targetMember); + var targetProperty = GetLastPropertyName(targetPath); + + // Add a conditional property mapping + Mappings.Add(new PropertyMapping + { + TargetProperty = targetProperty, + TargetPropertyPath = targetPath, + Condition = source => condition((TSource)source) + }); + + return this; + } + + public IMappingExpression MapWith(Expression> targetMember, Func transformFunction) + { + var targetPath = GetMemberPathForTarget(targetMember); + var targetProperty = GetLastPropertyName(targetPath); + + // For MapWith, we'll create a property mapping where the source property has the same name + // as the target property, but we'll apply the transformation function + var sourcePath = targetPath; // Assuming same property name for simplicity + + // Add a transformation property mapping + Mappings.Add(new PropertyMapping + { + SourceProperty = targetProperty, // Same name for source and target + TargetProperty = targetProperty, + SourcePropertyPath = sourcePath, + TargetPropertyPath = targetPath, + TransformFunction = transformFunction + }); + + return this; + } } } \ No newline at end of file diff --git a/src/TurboMapper/MappingModule.cs b/src/TurboMapper/MappingModule.cs index a8221a3..7596a6a 100644 --- a/src/TurboMapper/MappingModule.cs +++ b/src/TurboMapper/MappingModule.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace TurboMapper @@ -7,11 +8,13 @@ public abstract class MappingModule : IMappingModule { private readonly Action> _configAction; private readonly bool _enableDefaultMapping; + private readonly Dictionary _converters; public MappingModule(bool enableDefaultMapping = true) { _configAction = CreateMappings(); _enableDefaultMapping = enableDefaultMapping; + _converters = new Dictionary(); } void IMappingModule.CreateMap(IObjectMap mapper) @@ -51,8 +54,59 @@ void IMappingModule.CreateMap(IObjectMap mapper) } mapper.CreateMap(expression.Mappings, _enableDefaultMapping); + + // Register converters with the mapper if any were defined in this module + RegisterConverters(mapper); } public abstract Action> CreateMappings(); + + /// + /// Registers a custom converter for type mappings within this module + /// + /// Source type + /// Destination type + /// Function to perform the conversion + protected void RegisterConverter(Func converter) + { + var key = $"{typeof(TSourceConverter).FullName}_{typeof(TDestination).FullName}"; + _converters[key] = converter; + } + + private void RegisterConverters(IObjectMap mapper) + { + foreach (var kvp in _converters) + { + var converter = kvp.Value as Delegate; + if (converter != null) + { + var sourceType = converter.Method.GetParameters()[0].ParameterType; + var destType = converter.Method.ReturnType; + + // Find and invoke the RegisterConverter method with proper type parameters + var method = mapper.GetType().GetMethod("RegisterConverter", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, + null, + new Type[] { converter.GetType() }, + null); + + if (method == null) + { + // Try to get the generic method and make it specific + var genericMethod = mapper.GetType().GetMethod("RegisterConverter", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (genericMethod != null && genericMethod.IsGenericMethod) + { + var specificMethod = genericMethod.MakeGenericMethod(sourceType, destType); + specificMethod.Invoke(mapper, new object[] { converter }); + } + } + else + { + method.Invoke(mapper, new object[] { converter }); + } + } + } + } } } \ No newline at end of file diff --git a/src/TurboMapper/PropertyMapping.cs b/src/TurboMapper/PropertyMapping.cs index 119e910..a31b375 100644 --- a/src/TurboMapper/PropertyMapping.cs +++ b/src/TurboMapper/PropertyMapping.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace TurboMapper @@ -9,6 +10,9 @@ internal class PropertyMapping public string SourcePropertyPath { get; set; } public string TargetPropertyPath { get; set; } public bool IsNested { get; set; } + public bool IsIgnored { get; set; } + public Func Condition { get; set; } // For conditional mapping + public object TransformFunction { get; set; } // For transformation functions public List NestedMappings { get; set; } = new List(); } } \ No newline at end of file diff --git a/src/TurboMapper/TurboMapper.csproj b/src/TurboMapper/TurboMapper.csproj index 8332cc4..d768441 100644 --- a/src/TurboMapper/TurboMapper.csproj +++ b/src/TurboMapper/TurboMapper.csproj @@ -14,15 +14,15 @@ True Copyright (c) 2025 Code Shayk git - object-mapper, deep-mapper, shallow-mapper, mapping-library,automapper, turbomapper, objectmapper, mapper, mappinglibrary + object-mapper, deep-mapper, mapping-library, automapper, turbomapper, objectmapper, mapper, mappings, mapper-library True snupkg LICENSE True https://github.com/CodeShayk/TurboMapper/wiki https://github.com/CodeShayk/TurboMapper - v1.0.0 - Release of object mapper - 1.0.0 + v1.2.0 - Enhanced core and mapping features including performance improvements (2x+ speed), collection mapping support, custom type converters, conditional mapping, transformation functions, and configuration validation + 1.2.0 True TurboMapper diff --git a/src/TurboMapper/ValidationResult.cs b/src/TurboMapper/ValidationResult.cs new file mode 100644 index 0000000..5afda2b --- /dev/null +++ b/src/TurboMapper/ValidationResult.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace TurboMapper +{ + public class ValidationResult + { + public bool IsValid { get; set; } + public IEnumerable Errors { get; set; } + + public ValidationResult() + { + Errors = new List(); + } + + public ValidationResult(bool isValid, IEnumerable errors) + { + IsValid = isValid; + Errors = errors ?? new List(); + } + } +} \ No newline at end of file diff --git a/tests/TurboMapper.Tests/IntegrationTests.cs b/tests/TurboMapper.Tests/IntegrationTests.cs index 197e740..1de2c43 100644 --- a/tests/TurboMapper.Tests/IntegrationTests.cs +++ b/tests/TurboMapper.Tests/IntegrationTests.cs @@ -1,6 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; namespace TurboMapper.Tests { diff --git a/tests/TurboMapper.Tests/MapperAdvancedTests.cs b/tests/TurboMapper.Tests/MapperAdvancedTests.cs index 3d51f77..6645a0b 100644 --- a/tests/TurboMapper.Tests/MapperAdvancedTests.cs +++ b/tests/TurboMapper.Tests/MapperAdvancedTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using TurboMapper.Impl; namespace TurboMapper.Tests diff --git a/tests/TurboMapper.Tests/Release120_Tests.cs b/tests/TurboMapper.Tests/Release120_Tests.cs new file mode 100644 index 0000000..834d279 --- /dev/null +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -0,0 +1,327 @@ +using TurboMapper.Impl; + +namespace TurboMapper.Tests +{ + [TestFixture] + public class Release120_Tests + { + [Test] + public void Task1_1_RefactorDuplicatedGetMemberPathMethods() + { + // This is more of a code structure improvement test + // The functionality should remain the same, just with consolidated code + var mapper = new Mapper(); + + // Test that the mapping still works correctly after refactoring + //var config = new MappingModule(false); + var expression = new MappingExpression(); + + // The refactoring should not affect the functionality + Assert.IsNotNull(expression); + } + + [Test] + public void Task1_2_ReflectionMetadataCaching() + { + var mapper = new Mapper(); + + // Create a simple mapping to test caching + mapper.CreateMap(); + + // Map multiple times to test caching performance + var source = new Person { Name = "John", Age = 30 }; + var result1 = mapper.Map(source); + var result2 = mapper.Map(source); + + Assert.AreEqual("John", result1.Name); + Assert.AreEqual(30, result1.Age); + Assert.AreEqual(result1.Name, result2.Name); + Assert.AreEqual(result1.Age, result2.Age); + } + + [Test] + public void Task1_3_OptimizeObjectCreation() + { + var mapper = new Mapper(); + mapper.CreateMap(); + + var source = new Person { Name = "Jane", Age = 25 }; + var result = mapper.Map(source); + + Assert.IsNotNull(result); + Assert.AreEqual("Jane", result.Name); + Assert.AreEqual(25, result.Age); + } + + [Test] + public void Task1_4_SimplifyComplexMethods() + { + var mapper = new Mapper(); + mapper.CreateMap(); + + // Test default mapping functionality + var source = new Person { Name = "Bob", Age = 40 }; + var result = mapper.Map(source); + + Assert.AreEqual("Bob", result.Name); + Assert.AreEqual(40, result.Age); + } + + [Test] + public void Task2_1_CompiledExpressionTrees_Performance() + { + var mapper = new Mapper(); + mapper.CreateMap(); + + // Execute multiple mappings to verify compiled expressions work + for (int i = 0; i < 100; i++) + { + var source = new Person { Name = $"Person{i}", Age = 20 + i % 50 }; + var result = mapper.Map(source); + + Assert.AreEqual($"Person{i}", result.Name); + Assert.AreEqual(20 + i % 50, result.Age); + } + } + + [Test] + public void Task2_2_ConfigurationCaching() + { + var mapper = new Mapper(); + mapper.CreateMap(); + + // Test that configuration lookup works + var source = new Person { Name = "Cached", Age = 35 }; + var result = mapper.Map(source); + + Assert.AreEqual("Cached", result.Name); + Assert.AreEqual(35, result.Age); + } + + [Test] + public void Task3_1_CollectionMappingSupport() + { + var mapper = new Mapper(); + mapper.CreateMap(); + + var people = new List + { + new Person { Name = "Alice", Age = 28 }, + new Person { Name = "Bob", Age = 32 } + }; + + // Test collection mapping using the new Map method that returns IEnumerable + var peopleDto = mapper.Map(people); + var peopleDtoList = peopleDto.ToList(); // Convert to list to access Count and indexer + + Assert.AreEqual(2, peopleDtoList.Count); + Assert.AreEqual("Alice", peopleDtoList[0].Name); + Assert.AreEqual(28, peopleDtoList[0].Age); + Assert.AreEqual("Bob", peopleDtoList[1].Name); + Assert.AreEqual(32, peopleDtoList[1].Age); + } + + [Test] + public void Task3_4_IgnoredPropertiesOption() + { + var mapper = new Mapper(); + + // Create a custom mapping with ignored properties + var expression = new MappingExpression(); + expression.Ignore(x => x.Age); // Ignore the Age property + + // Simulate adding this to configuration (simplified test) + var mappings = expression.Mappings; + var ignoredMapping = mappings.FirstOrDefault(m => m.IsIgnored && m.TargetProperty == "Age"); + + Assert.IsNotNull(ignoredMapping); + Assert.IsTrue(ignoredMapping.IsIgnored); + } + + [Test] + public void Task5_1_CustomTypeConvertersRegistration() + { + // Create a mapping from string to int using converter registered in module + var converterModule = new StringToIntConverterModule(); + var mapper = new Mapper(); + + // Register the module which will register the string to int converter + ((IMappingModule)converterModule).CreateMap(mapper); + + // Test that a string value can be converted to int through the registered converter + var source = new StringValueClass { Number = "42" }; + var result = mapper.Map(source); + + Assert.AreEqual(42, result.Number); + } + + // Test module for converter registration + public class StringToIntConverterModule : MappingModule + { + public StringToIntConverterModule() : base(true) // Enable default mapping for this test + { + // Register converter within the module + RegisterConverter(s => int.Parse(s)); + } + + public override Action> CreateMappings() + { + return expression => { }; + } + } + + public class StringValueClass + { + public string Number { get; set; } + } + + public class IntValueClass + { + public int Number { get; set; } + } + + [Test] + public void Task5_2_ImprovedNullableTypeHandling() + { + var mapper = new Mapper(); + + // Test nullable to non-nullable conversion + int? nullableInt = 42; + var result = mapper.GetType().GetMethod("ConvertValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(mapper, new object[] { nullableInt, typeof(int) }); + + Assert.AreEqual(42, result); + + // Test non-nullable to nullable conversion + int nonNullableInt = 35; + var result2 = mapper.GetType().GetMethod("ConvertValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(mapper, new object[] { nonNullableInt, typeof(int?) }); + + Assert.AreEqual(35, result2); + } + + [Test] + public void Task6_1_ImprovedErrorMessages() + { + var mapper = new Mapper(); + + try + { + // Try to convert an invalid string to int to trigger error handling + var result = mapper.GetType().GetMethod("ConvertValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(mapper, new object[] { "invalid_number", typeof(int) }); + + // If we reach here without exception, there's an issue + Assert.Fail("Expected exception was not thrown"); + } + catch (System.Reflection.TargetInvocationException ex) + { + // Check that the inner exception contains helpful information + Assert.IsTrue(ex.InnerException.Message.Contains("convert") || ex.InnerException.Message.Contains("Failed")); + } + } + + [Test] + public void Task3_2_ConditionalMapping() + { + var mapper = new Mapper(); + + // Create a conditional mapping expression + var expression = new MappingExpression(); + + // Add a conditional mapping (simplified test) + expression.When(x => x.Name, p => p.Age > 18); + + var mappings = expression.Mappings; + var conditionalMapping = mappings.FirstOrDefault(m => m.Condition != null); + + Assert.IsNotNull(conditionalMapping); + } + + [Test] + public void Task3_3_MappingWithTransformation() + { + var mapper = new Mapper(); + + // Create a transformation mapping expression + var expression = new MappingExpression(); + expression.MapWith(p => p.Name, p => $"Mr. {p.Name}"); + + var mappings = expression.Mappings; + var transformationMapping = mappings.FirstOrDefault(m => m.TransformFunction != null); + + Assert.IsNotNull(transformationMapping); + } + + [Test] + public void Task5_4_ComprehensiveBuiltInTypeConversions() + { + var mapper = new Mapper(); + + // Test various type conversions + var conversions = new (object Value, Type Target, object Expected)[] + { + ((object)3.14, typeof(float), 3.14f), + ((object)100, typeof(long), 100L), + ((object)42.5f, typeof(double), 42.5), + ((object)123, typeof(decimal), 123m), + ((object)"2023-01-01", typeof(DateTime), new DateTime(2023, 1, 1)) + }; + + foreach (var conversion in conversions) + { + var result = mapper.GetType().GetMethod("ConvertValue", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.Invoke(mapper, new object[] { conversion.Value, conversion.Target }); + + Assert.AreEqual(conversion.Expected, result, + $"Conversion from {conversion.Value.GetType()} to {conversion.Target} failed"); + } + } + + [Test] + public void Task6_2_ConfigurationValidation() + { + var mapper = new Mapper(); + + // Create a valid mapping configuration + mapper.CreateMap(new List()); + + // Validate the mapping + var validationResult = mapper.ValidateMapping(); + + Assert.IsTrue(validationResult.IsValid, string.Join(", ", validationResult.Errors)); + Assert.AreEqual(0, validationResult.Errors.Count()); + } + + // Test models + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + public Address Address { get; set; } + } + + public class PersonDto + { + public string Name { get; set; } + public int Age { get; set; } + public AddressDto Address { get; set; } + } + + public class Address + { + public string Street { get; set; } + public string City { get; set; } + } + + public class AddressDto + { + public string Street { get; set; } + public string City { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/TurboMapper.Tests/ServiceCollectionExtensionTests.cs b/tests/TurboMapper.Tests/ServiceCollectionExtensionTests.cs index 4fee67c..7ae29be 100644 --- a/tests/TurboMapper.Tests/ServiceCollectionExtensionTests.cs +++ b/tests/TurboMapper.Tests/ServiceCollectionExtensionTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; using Microsoft.Extensions.DependencyInjection; -using TurboMapper; -using TurboMapper.Tests; namespace TurboMapper.Tests {