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 @@
-#
TurboMapper v1.0.0
+#
TurboMapper v1.2.0
[](https://badge.fury.io/nu/TurboMapper) [](https://github.com/CodeShayk/TurboMapper/blob/master/LICENSE.md)
[](https://github.com/CodeShayk/TurboMapper/releases/latest)
[](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