From d3198db124a2bf65685b544a82dfa05b0dc4cdc3 Mon Sep 17 00:00:00 2001 From: Ninja Date: Sat, 11 Oct 2025 12:46:25 +0100 Subject: [PATCH 1/6] - checkpoint - Initial v1.2.0 --- tests/TurboMapper.Tests/Release120_Tests.cs | 306 ++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 tests/TurboMapper.Tests/Release120_Tests.cs diff --git a/tests/TurboMapper.Tests/Release120_Tests.cs b/tests/TurboMapper.Tests/Release120_Tests.cs new file mode 100644 index 0000000..3e80a29 --- /dev/null +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TurboMapper; +using TurboMapper.Impl; + +namespace TurboMapper.Tests +{ + [TestClass] + public class Release120_Tests + { + [TestMethod] + 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); + } + + [TestMethod] + 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); + } + + [TestMethod] + 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); + } + + [TestMethod] + 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); + } + + [TestMethod] + 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); + } + } + + [TestMethod] + 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); + } + + [TestMethod] + 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 + var peopleDto = mapper.MapList(people); + + Assert.AreEqual(2, peopleDto.Count); + Assert.AreEqual("Alice", peopleDto[0].Name); + Assert.AreEqual(28, peopleDto[0].Age); + Assert.AreEqual("Bob", peopleDto[1].Name); + Assert.AreEqual(32, peopleDto[1].Age); + } + + [TestMethod] + 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); + } + + [TestMethod] + public void Task5_1_CustomTypeConvertersRegistration() + { + var mapper = new Mapper(); + + // Register a custom converter + mapper.RegisterConverter(s => int.Parse(s)); + mapper.RegisterConverter(i => i.ToString()); + + // Verify the converter is registered by trying a simple conversion + // Note: This test may need adjustment based on how the converter system is fully implemented + var convertersExist = true; // Placeholder - actual test would check internal state + + Assert.IsTrue(convertersExist); + } + + [TestMethod] + 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); + } + + [TestMethod] + 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")); + } + } + + [TestMethod] + 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); + } + + [TestMethod] + 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); + } + + [TestMethod] + public void Task5_4_ComprehensiveBuiltInTypeConversions() + { + var mapper = new Mapper(); + + // Test various type conversions + var conversions = new[] + { + (Value: (object)3.14, Target: typeof(float), Expected: 3.14f), + (Value: (object)100, Target: typeof(long), Expected: 100L), + (Value: (object)42.5f, Target: typeof(double), Expected: 42.5), + (Value: (object)123, Target: typeof(decimal), Expected: 123m), + (Value: (object)"2023-01-01", Target: typeof(DateTime), Expected: 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"); + } + } + + [TestMethod] + public void Task6_2_ConfigurationValidation() + { + var mapper = new Mapper(); + + // Create a valid mapping configuration + mapper.CreateMap(new List()); + + // Validate the mapping + var isValid = mapper.ValidateMapping(); + var errors = mapper.GetMappingErrors(); + + Assert.IsTrue(isValid, string.Join(", ", errors)); + Assert.AreEqual(0, errors.Length); + } + + // 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 From 61a7d2971763812fd8ebfff2bda5b6e4773dae62 Mon Sep 17 00:00:00 2001 From: Ninja Date: Sat, 11 Oct 2025 12:56:17 +0100 Subject: [PATCH 2/6] - checkin refactoring --- README.md | 66 ++- src/TurboMapper/IMapper.cs | 7 + src/TurboMapper/IMappingExpression.cs | 3 + src/TurboMapper/Impl/Mapper.cs | 610 ++++++++++++++++---- src/TurboMapper/MappingExpression.cs | 77 ++- src/TurboMapper/PropertyMapping.cs | 4 + src/TurboMapper/TurboMapper.csproj | 4 +- tests/TurboMapper.Tests/Release120_Tests.cs | 158 +++-- 8 files changed, 725 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index c4528d0..af15032 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,29 +10,85 @@ ## 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); + +// Convert to list if needed +List targetList = targets.ToList(); +``` + +### 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 | Please see [Release Roadmap](https://github.com/CodeShayk/TurboMapper/blob/master/Release_Roadmap.md) for more details. +## Key Features in Release 1.2.0 +- **Performance Improvements**: Significant performance enhancements (2x+) through compiled expression trees and metadata caching +- **Enhanced Collection Mapping**: Simplified API with Map method now supporting both single objects and collections +- **Ignored Properties Option**: Added Ignore method to IMappingExpression to skip properties during mapping +- **Custom Type Converters Registration**: Added RegisterConverter method to IMapper for custom type conversion functions +- **Improved Nullable Type Handling**: Enhanced ConvertValue method to handle nullable types properly +- **Conditional Mapping**: Added When method to IMappingExpression for conditional property mapping +- **Mapping Transformations**: Added MapWith method for transformation functions during mapping +- **Comprehensive Type Conversions**: Enhanced ConvertValue with DateTime, TimeSpan, and other common type conversions +- **Configuration Validation**: Added ValidateMapping and GetMappingErrors methods to IMapper for early validation +- **Improved Error Messages**: Better debugging information for conversion failures + ## Contributing We welcome contributions! Please see our Contributing Guide for details. - 🐛 Bug Reports - If you are having problems, please let me know by raising a [new issue](https://github.com/CodeShayk/TurboMapper/issues/new/choose). diff --git a/src/TurboMapper/IMapper.cs b/src/TurboMapper/IMapper.cs index 510fc56..3041c39 100644 --- a/src/TurboMapper/IMapper.cs +++ b/src/TurboMapper/IMapper.cs @@ -1,7 +1,14 @@ +using System; +using System.Collections.Generic; + namespace TurboMapper { public interface IMapper { TTarget Map(TSource source); + IEnumerable Map(IEnumerable source); + void RegisterConverter(Func converter); + bool ValidateMapping(); + string[] GetMappingErrors(); } } \ No newline at end of file diff --git a/src/TurboMapper/IMappingExpression.cs b/src/TurboMapper/IMappingExpression.cs index ce0da01..70da1b8 100644 --- a/src/TurboMapper/IMappingExpression.cs +++ b/src/TurboMapper/IMappingExpression.cs @@ -6,5 +6,8 @@ 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/Impl/Mapper.cs b/src/TurboMapper/Impl/Mapper.cs index 7cdc14f..6d6266c 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,11 +134,26 @@ 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); + } + } } } @@ -111,8 +162,8 @@ private void ApplyDefaultNameBasedMapping( 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 +173,121 @@ 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); + } - if (IsComplexType(sourceProp.PropertyType) && IsComplexType(targetProp.PropertyType)) + private bool IsIgnoredInCustomMappings(string targetPropertyName, List customMappings) + { + return customMappings.Exists(m => + m.TargetPropertyPath.Split('.').Last() == targetPropertyName && m.IsIgnored); + } + + // Custom converter system + private readonly Dictionary _converters = new Dictionary(); + + public void RegisterConverter(Func converter) + { + var key = $"{typeof(TSource).FullName}_{typeof(TDestination).FullName}"; + _converters[key] = converter; + } + + public bool ValidateMapping() + { + var errors = GetMappingErrors(); + return errors.Length == 0; + } + + public string[] GetMappingErrors() + { + 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}'"); + } } } } } + + return errors.ToArray(); + } + + 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,41 +296,87 @@ 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) { var properties = propertyPath.Split('.'); @@ -224,11 +387,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 +406,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 +449,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 +627,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 +651,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..5ba1b2b 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/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..1f09fd1 100644 --- a/src/TurboMapper/TurboMapper.csproj +++ b/src/TurboMapper/TurboMapper.csproj @@ -21,8 +21,8 @@ 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/tests/TurboMapper.Tests/Release120_Tests.cs b/tests/TurboMapper.Tests/Release120_Tests.cs index 3e80a29..641498b 100644 --- a/tests/TurboMapper.Tests/Release120_Tests.cs +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -1,197 +1,193 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using TurboMapper; using TurboMapper.Impl; namespace TurboMapper.Tests { - [TestClass] + [TestFixture] public class Release120_Tests { - [TestMethod] + [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 config = new MappingModule(false); var expression = new MappingExpression(); - + // The refactoring should not affect the functionality Assert.IsNotNull(expression); } - [TestMethod] + [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); } - [TestMethod] + [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); } - [TestMethod] + [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); } - [TestMethod] + [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); } } - [TestMethod] + [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); } - [TestMethod] + [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 - var peopleDto = mapper.MapList(people); - - Assert.AreEqual(2, peopleDto.Count); - Assert.AreEqual("Alice", peopleDto[0].Name); - Assert.AreEqual(28, peopleDto[0].Age); - Assert.AreEqual("Bob", peopleDto[1].Name); - Assert.AreEqual(32, peopleDto[1].Age); + + // 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); } - [TestMethod] + [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); } - [TestMethod] + [Test] public void Task5_1_CustomTypeConvertersRegistration() { var mapper = new Mapper(); - + // Register a custom converter mapper.RegisterConverter(s => int.Parse(s)); mapper.RegisterConverter(i => i.ToString()); - + // Verify the converter is registered by trying a simple conversion // Note: This test may need adjustment based on how the converter system is fully implemented var convertersExist = true; // Placeholder - actual test would check internal state - + Assert.IsTrue(convertersExist); } - [TestMethod] + [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", + 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", + 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); } - [TestMethod] + [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", + 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"); } @@ -202,76 +198,76 @@ public void Task6_1_ImprovedErrorMessages() } } - [TestMethod] + [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); } - [TestMethod] + [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); } - [TestMethod] + [Test] public void Task5_4_ComprehensiveBuiltInTypeConversions() { var mapper = new Mapper(); - + // Test various type conversions - var conversions = new[] + var conversions = new (object Value, Type Target, object Expected)[] { - (Value: (object)3.14, Target: typeof(float), Expected: 3.14f), - (Value: (object)100, Target: typeof(long), Expected: 100L), - (Value: (object)42.5f, Target: typeof(double), Expected: 42.5), - (Value: (object)123, Target: typeof(decimal), Expected: 123m), - (Value: (object)"2023-01-01", Target: typeof(DateTime), Expected: new DateTime(2023, 1, 1)) + ((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", + 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, + + Assert.AreEqual(conversion.Expected, result, $"Conversion from {conversion.Value.GetType()} to {conversion.Target} failed"); } } - [TestMethod] + [Test] public void Task6_2_ConfigurationValidation() { var mapper = new Mapper(); - + // Create a valid mapping configuration mapper.CreateMap(new List()); - + // Validate the mapping var isValid = mapper.ValidateMapping(); var errors = mapper.GetMappingErrors(); - + Assert.IsTrue(isValid, string.Join(", ", errors)); Assert.AreEqual(0, errors.Length); } From 81d28873b9fa80e398204ce564b33bfb69cdf071 Mon Sep 17 00:00:00 2001 From: Ninja Date: Sat, 11 Oct 2025 23:24:06 +0100 Subject: [PATCH 3/6] - checkpoint: Move converter registration to mapping module --- README.md | 2 +- src/TurboMapper/IMapper.cs | 1 - src/TurboMapper/IObjectMap.cs | 3 ++ src/TurboMapper/Impl/Mapper.cs | 12 ++--- src/TurboMapper/MappingModule.cs | 54 +++++++++++++++++++++ tests/TurboMapper.Tests/Release120_Tests.cs | 46 ++++++++++++++---- 6 files changed, 100 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index af15032..7096e3c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Please see [Release Roadmap](https://github.com/CodeShayk/TurboMapper/blob/maste - **Performance Improvements**: Significant performance enhancements (2x+) through compiled expression trees and metadata caching - **Enhanced Collection Mapping**: Simplified API with Map method now supporting both single objects and collections - **Ignored Properties Option**: Added Ignore method to IMappingExpression to skip properties during mapping -- **Custom Type Converters Registration**: Added RegisterConverter method to IMapper for custom type conversion functions +- **Custom Type Converters Registration**: Added RegisterConverter method to MappingModule for custom type conversion functions that can be registered with type mappings - **Improved Nullable Type Handling**: Enhanced ConvertValue method to handle nullable types properly - **Conditional Mapping**: Added When method to IMappingExpression for conditional property mapping - **Mapping Transformations**: Added MapWith method for transformation functions during mapping diff --git a/src/TurboMapper/IMapper.cs b/src/TurboMapper/IMapper.cs index 3041c39..404bdba 100644 --- a/src/TurboMapper/IMapper.cs +++ b/src/TurboMapper/IMapper.cs @@ -7,7 +7,6 @@ public interface IMapper { TTarget Map(TSource source); IEnumerable Map(IEnumerable source); - void RegisterConverter(Func converter); bool ValidateMapping(); string[] GetMappingErrors(); } diff --git a/src/TurboMapper/IObjectMap.cs b/src/TurboMapper/IObjectMap.cs index 0f845f2..1828203 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 6d6266c..3a91a7a 100644 --- a/src/TurboMapper/Impl/Mapper.cs +++ b/src/TurboMapper/Impl/Mapper.cs @@ -157,6 +157,12 @@ internal void ApplyCustomMappingsWithDefaultDisabled( } } + public void RegisterConverter(Func converter) + { + var key = $"{typeof(TSource).FullName}_{typeof(TDestination).FullName}"; + _converters[key] = converter; + } + private void ApplyDefaultNameBasedMapping( TSource source, TTarget target, @@ -195,12 +201,6 @@ private bool IsIgnoredInCustomMappings(string targetPropertyName, List _converters = new Dictionary(); - public void RegisterConverter(Func converter) - { - var key = $"{typeof(TSource).FullName}_{typeof(TDestination).FullName}"; - _converters[key] = converter; - } - public bool ValidateMapping() { var errors = GetMappingErrors(); diff --git a/src/TurboMapper/MappingModule.cs b/src/TurboMapper/MappingModule.cs index a8221a3..03e77d4 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/tests/TurboMapper.Tests/Release120_Tests.cs b/tests/TurboMapper.Tests/Release120_Tests.cs index 641498b..57be794 100644 --- a/tests/TurboMapper.Tests/Release120_Tests.cs +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -141,17 +141,43 @@ public void Task3_4_IgnoredPropertiesOption() [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 a custom converter - mapper.RegisterConverter(s => int.Parse(s)); - mapper.RegisterConverter(i => i.ToString()); - - // Verify the converter is registered by trying a simple conversion - // Note: This test may need adjustment based on how the converter system is fully implemented - var convertersExist = true; // Placeholder - actual test would check internal state - - Assert.IsTrue(convertersExist); + + // 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] From fc076a70c41f1f23dd8e45c741e03c6e16290b2e Mon Sep 17 00:00:00 2001 From: Ninja Date: Sat, 11 Oct 2025 23:48:23 +0100 Subject: [PATCH 4/6] - checkpoint: refactor configuration validation --- README.md | 2 +- src/TurboMapper/IMapper.cs | 5 +++-- src/TurboMapper/Impl/Mapper.cs | 11 +++-------- src/TurboMapper/ValidationResult.cs | 21 +++++++++++++++++++++ tests/TurboMapper.Tests/Release120_Tests.cs | 7 +++---- 5 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/TurboMapper/ValidationResult.cs diff --git a/README.md b/README.md index 7096e3c..10a3ef4 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Please see [Release Roadmap](https://github.com/CodeShayk/TurboMapper/blob/maste - **Conditional Mapping**: Added When method to IMappingExpression for conditional property mapping - **Mapping Transformations**: Added MapWith method for transformation functions during mapping - **Comprehensive Type Conversions**: Enhanced ConvertValue with DateTime, TimeSpan, and other common type conversions -- **Configuration Validation**: Added ValidateMapping and GetMappingErrors methods to IMapper for early validation +- **Configuration Validation**: Added ValidateMapping method to IMapper that returns ValidationResult for early validation - **Improved Error Messages**: Better debugging information for conversion failures ## Contributing diff --git a/src/TurboMapper/IMapper.cs b/src/TurboMapper/IMapper.cs index 404bdba..445d950 100644 --- a/src/TurboMapper/IMapper.cs +++ b/src/TurboMapper/IMapper.cs @@ -6,8 +6,9 @@ namespace TurboMapper public interface IMapper { TTarget Map(TSource source); + IEnumerable Map(IEnumerable source); - bool ValidateMapping(); - string[] GetMappingErrors(); + + ValidationResult ValidateMapping(); } } \ No newline at end of file diff --git a/src/TurboMapper/Impl/Mapper.cs b/src/TurboMapper/Impl/Mapper.cs index 3a91a7a..38384df 100644 --- a/src/TurboMapper/Impl/Mapper.cs +++ b/src/TurboMapper/Impl/Mapper.cs @@ -201,13 +201,7 @@ private bool IsIgnoredInCustomMappings(string targetPropertyName, List _converters = new Dictionary(); - public bool ValidateMapping() - { - var errors = GetMappingErrors(); - return errors.Length == 0; - } - - public string[] GetMappingErrors() + public ValidationResult ValidateMapping() { var errors = new List(); var sourceType = typeof(TSource); @@ -241,7 +235,8 @@ public string[] GetMappingErrors() } } - return errors.ToArray(); + var isValid = errors.Count == 0; + return new ValidationResult(isValid, errors); } private bool PropertyExists(Type type, string propertyPath) diff --git a/src/TurboMapper/ValidationResult.cs b/src/TurboMapper/ValidationResult.cs new file mode 100644 index 0000000..0665c80 --- /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/Release120_Tests.cs b/tests/TurboMapper.Tests/Release120_Tests.cs index 57be794..5a4aad5 100644 --- a/tests/TurboMapper.Tests/Release120_Tests.cs +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -291,11 +291,10 @@ public void Task6_2_ConfigurationValidation() mapper.CreateMap(new List()); // Validate the mapping - var isValid = mapper.ValidateMapping(); - var errors = mapper.GetMappingErrors(); + var validationResult = mapper.ValidateMapping(); - Assert.IsTrue(isValid, string.Join(", ", errors)); - Assert.AreEqual(0, errors.Length); + Assert.IsTrue(validationResult.IsValid, string.Join(", ", validationResult.Errors)); + Assert.AreEqual(0, validationResult.Errors.Count()); } // Test models From fb2bae8a804befeff10476275cb51c48e2abe3d5 Mon Sep 17 00:00:00 2001 From: Ninja Date: Sun, 12 Oct 2025 00:01:00 +0100 Subject: [PATCH 5/6] - checkpoint: update gitversion --- GitVersion.yml | 2 +- README.md | 15 --------------- src/TurboMapper/TurboMapper.csproj | 2 +- 3 files changed, 2 insertions(+), 17 deletions(-) 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 10a3ef4..a749c32 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,6 @@ var sources = new List // Map to IEnumerable IEnumerable targets = mapper.Map(sources); - -// Convert to list if needed -List targetList = targets.ToList(); ``` ### iii. Developer Guide @@ -77,18 +74,6 @@ This section provides the summary of planned releases with key details about eac Please see [Release Roadmap](https://github.com/CodeShayk/TurboMapper/blob/master/Release_Roadmap.md) for more details. -## Key Features in Release 1.2.0 -- **Performance Improvements**: Significant performance enhancements (2x+) through compiled expression trees and metadata caching -- **Enhanced Collection Mapping**: Simplified API with Map method now supporting both single objects and collections -- **Ignored Properties Option**: Added Ignore method to IMappingExpression to skip properties during mapping -- **Custom Type Converters Registration**: Added RegisterConverter method to MappingModule for custom type conversion functions that can be registered with type mappings -- **Improved Nullable Type Handling**: Enhanced ConvertValue method to handle nullable types properly -- **Conditional Mapping**: Added When method to IMappingExpression for conditional property mapping -- **Mapping Transformations**: Added MapWith method for transformation functions during mapping -- **Comprehensive Type Conversions**: Enhanced ConvertValue with DateTime, TimeSpan, and other common type conversions -- **Configuration Validation**: Added ValidateMapping method to IMapper that returns ValidationResult for early validation -- **Improved Error Messages**: Better debugging information for conversion failures - ## Contributing We welcome contributions! Please see our Contributing Guide for details. - 🐛 Bug Reports - If you are having problems, please let me know by raising a [new issue](https://github.com/CodeShayk/TurboMapper/issues/new/choose). diff --git a/src/TurboMapper/TurboMapper.csproj b/src/TurboMapper/TurboMapper.csproj index 1f09fd1..d768441 100644 --- a/src/TurboMapper/TurboMapper.csproj +++ b/src/TurboMapper/TurboMapper.csproj @@ -14,7 +14,7 @@ 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 From 8a25ce39baf9b8812735715f602a648420b93c9a Mon Sep 17 00:00:00 2001 From: Ninja Date: Sun, 12 Oct 2025 00:10:35 +0100 Subject: [PATCH 6/6] - fix lint issues --- src/TurboMapper/IMapper.cs | 1 - src/TurboMapper/IMappingExpression.cs | 3 +++ src/TurboMapper/IObjectMap.cs | 2 +- src/TurboMapper/MappingExpression.cs | 6 +++--- src/TurboMapper/MappingModule.cs | 10 +++++----- src/TurboMapper/ValidationResult.cs | 4 ++-- tests/TurboMapper.Tests/IntegrationTests.cs | 2 -- tests/TurboMapper.Tests/MapperAdvancedTests.cs | 2 -- tests/TurboMapper.Tests/Release120_Tests.cs | 14 +++++++------- .../ServiceCollectionExtensionTests.cs | 4 ---- 10 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/TurboMapper/IMapper.cs b/src/TurboMapper/IMapper.cs index 445d950..3bbb173 100644 --- a/src/TurboMapper/IMapper.cs +++ b/src/TurboMapper/IMapper.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace TurboMapper diff --git a/src/TurboMapper/IMappingExpression.cs b/src/TurboMapper/IMappingExpression.cs index 70da1b8..947b0e4 100644 --- a/src/TurboMapper/IMappingExpression.cs +++ b/src/TurboMapper/IMappingExpression.cs @@ -6,8 +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 1828203..3fe6038 100644 --- a/src/TurboMapper/IObjectMap.cs +++ b/src/TurboMapper/IObjectMap.cs @@ -8,7 +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/MappingExpression.cs b/src/TurboMapper/MappingExpression.cs index 5ba1b2b..f6dc950 100644 --- a/src/TurboMapper/MappingExpression.cs +++ b/src/TurboMapper/MappingExpression.cs @@ -71,7 +71,7 @@ public IMappingExpression Ignore(Expression When(Expression> targetMember, Func condition) { var targetPath = GetMemberPathForTarget(targetMember); @@ -87,7 +87,7 @@ public IMappingExpression When(Expression MapWith(Expression> targetMember, Func transformFunction) { var targetPath = GetMemberPathForTarget(targetMember); @@ -96,7 +96,7 @@ public IMappingExpression MapWith( // 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 { diff --git a/src/TurboMapper/MappingModule.cs b/src/TurboMapper/MappingModule.cs index 03e77d4..7596a6a 100644 --- a/src/TurboMapper/MappingModule.cs +++ b/src/TurboMapper/MappingModule.cs @@ -82,14 +82,14 @@ private void RegisterConverters(IObjectMap mapper) { 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", + var method = mapper.GetType().GetMethod("RegisterConverter", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, - null, - new Type[] { converter.GetType() }, + null, + new Type[] { converter.GetType() }, null); - + if (method == null) { // Try to get the generic method and make it specific diff --git a/src/TurboMapper/ValidationResult.cs b/src/TurboMapper/ValidationResult.cs index 0665c80..5afda2b 100644 --- a/src/TurboMapper/ValidationResult.cs +++ b/src/TurboMapper/ValidationResult.cs @@ -6,12 +6,12 @@ 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; 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 index 5a4aad5..834d279 100644 --- a/tests/TurboMapper.Tests/Release120_Tests.cs +++ b/tests/TurboMapper.Tests/Release120_Tests.cs @@ -144,17 +144,17 @@ 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 { @@ -163,18 +163,18 @@ public class StringToIntConverterModule : MappingModule(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; } 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 {