diff --git a/src/Mapster.Tool.Tests/Helpers/ConfigHelpers.cs b/src/Mapster.Tool.Tests/Helpers/ConfigHelpers.cs
new file mode 100644
index 00000000..057a7518
--- /dev/null
+++ b/src/Mapster.Tool.Tests/Helpers/ConfigHelpers.cs
@@ -0,0 +1,11 @@
+using System.Reflection;
+
+namespace Mapster.Tool.Tests.Helpers
+{
+ internal static class ConfigHelpers
+ {
+ internal static MapperOptions optMappers => new MapperOptions() { Assembly = Assembly.GetExecutingAssembly().Location, Output = Path.GetTempPath() };
+ internal static ModelOptions optModels = new ModelOptions() { Assembly = Assembly.GetExecutingAssembly().Location, Output = Path.GetTempPath() };
+ internal static ExtensionOptions optExtentions = new ExtensionOptions() { Assembly = Assembly.GetExecutingAssembly().Location, Output = Path.GetTempPath() };
+ }
+}
diff --git a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj
index 32897bb6..2bae4834 100644
--- a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj
+++ b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj
@@ -4,14 +4,11 @@
net10.0;net9.0;net8.0
enable
enable
+ $(MapsterToolTFMs)
true
false
-
- $(TargetFrameworks);net48
-
-
diff --git a/src/Mapster.Tool.Tests/Usings.cs b/src/Mapster.Tool.Tests/Usings.cs
index 8c927eb7..3bfc7862 100644
--- a/src/Mapster.Tool.Tests/Usings.cs
+++ b/src/Mapster.Tool.Tests/Usings.cs
@@ -1 +1,3 @@
-global using Xunit;
\ No newline at end of file
+global using Xunit;
+global using Mapster.Tool;
+global using Mapster.Tool.Tests.Helpers;
\ No newline at end of file
diff --git a/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs b/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs
index 7908b453..98009630 100644
--- a/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs
+++ b/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs
@@ -22,6 +22,57 @@ public void MapWithReflection()
userMapper.MapTo(user, dto);
dto.Name.Should().Be(expected);
}
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/1017
+ ///
+ [Fact]
+ public void CreateDtoWithcustomResolver()
+ {
+ var mappers = new List();
+
+ Generators.GenerateExtensions(ConfigHelpers.optExtentions, mappers);
+
+ var result = mappers.Where(x => x.Contains("User1017Dto AdaptToDto(this User1017")).FirstOrDefault();
+
+ result.Should().NotBeNullOrEmpty();
+ result.Contains("FullName = string.Format(\"{0} {1}\", p1.FirstName, p1.LastName)").Should().BeTrue();
+ }
+}
+
+
+public class User1017
+{
+ public int Id { get; set; }
+ public string Email { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public int Age { get; set; }
+}
+
+public partial class User1017Dto
+{
+ public int Id { get; set; }
+ public string Email { get; set; }
+ public string FullName { get; set; }
+ public int Age { get; set; }
+}
+
+
+public class UserCodeGenConfig : ICodeGenerationRegister
+{
+ public void Register(CodeGenerationConfig config)
+ {
+ config.AdaptTo("[name]Dto", MapType.Map)
+ .ForType(p =>
+ {
+ p.Ignore(s => s.FirstName);
+ p.Map(s => s.LastName, s => $"{s.FirstName} {s.LastName}", "FullName");
+ });
+
+ config.GenerateMapper("[name]Mapper")
+ .ForType();
+ }
}
public class UserMappingRegister : IRegister
diff --git a/src/Mapster.Tool/Generators.cs b/src/Mapster.Tool/Generators.cs
new file mode 100644
index 00000000..224f93b6
--- /dev/null
+++ b/src/Mapster.Tool/Generators.cs
@@ -0,0 +1,746 @@
+using CommandLine;
+using ExpressionDebugger;
+using ExpressionDebugger.Helpers;
+using ExpressionDebugger.Helpers.GeneratedAttributes;
+using Mapster.Models;
+using Mapster.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.Loader;
+using System.Text;
+
+[assembly:InternalsVisibleTo("Mapster.Tool.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd523e79e4decc052a3501363d71ecc123b9ce4bd5a8c949e81bc482d8b6822366ed6aead5ebace01aae3ade49e116fde094af03c34cdbc2ebcb89346ca510fac6246b240b71968ab7f9a24de44d680dc93307f9e8a2b00bec7c523db9696679b56725d622cfb01f4eb2604333a0a0e9f580cd6f5c3d5034b3e66f52d818e9a5")]
+namespace Mapster.Tool
+{
+ internal static class Generators
+ {
+
+ private static string? GetSegments(string? ns, string? baseNs)
+ {
+ if (ns == null || string.IsNullOrEmpty(baseNs) || baseNs == ns)
+ return null;
+ return ns.StartsWith(baseNs + ".") ? ns.Substring(baseNs.Length + 1) : ns;
+ }
+
+ private static string? CreateNamespace(string? ns, string? segment, string? typeNs)
+ {
+ if (ns == null)
+ return typeNs;
+ return segment == null ? ns : $"{ns}.{segment}";
+ }
+
+ private static string GetOutput(string baseOutput, string? segment, string typeName)
+ {
+ var fullBasePath = Path.GetFullPath(baseOutput);
+ return segment == null
+ ? Path.Combine(fullBasePath, typeName + ".g.cs")
+ : Path.Combine(
+ fullBasePath,
+ segment.Replace('.', Path.DirectorySeparatorChar),
+ typeName + ".g.cs"
+ );
+ }
+
+ private static void WriteFile(string code, string path)
+ {
+ var dir = Path.GetDirectoryName(path);
+ if (dir != null)
+ Directory.CreateDirectory(dir);
+ if (File.Exists(path))
+ {
+ var old = File.ReadAllText(path);
+ if (old == code)
+ return;
+ }
+ File.WriteAllText(path, code);
+ }
+
+ internal static void GenerateMappers(MapperOptions opt, List? DebugMappers = null)
+ {
+ // We want loaded assemblies that we're scanning to be isolated from our currently
+ // running assembly load context in order to avoid type/framework collisions between Mapster assemblies
+ // and their dependencies and the scanned assemblies and their dependencies
+
+ // However, we also need *some* of those scanned assemblies and thus their types to resolve from our
+ // currently running AssemblyLoadContext.Default: The Mapster assembly basically.
+
+ // This way when we compare attribute types (such as MapperAttribute) between our running assembly
+ // and the scanned assembly the two types with the same FullName can be considered equal because
+ // they both were resolved from AssemblyLoadContext.Default.
+
+ // This isolated Assembly Load Context will be able to resolve the Mapster assembly, but
+ // the resolved Assembly will be the same one that is in AssemblyLoadContext.Default
+ // (the runtime assembly load context that our code refers to by default when referencing
+ // types)
+ var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
+ assemblyPath: Path.GetFullPath(opt.Assembly),
+ deferToContext: AssemblyLoadContext.Default,
+ typeof(MapperAttribute).Assembly.GetName(),
+ typeof(IRegister).Assembly.GetName()
+ );
+ var config = TypeAdapterConfig.GlobalSettings;
+ config.SelfContainedCodeGeneration = true;
+ config.Scan(assembly);
+
+ var generatedAtrr = new List();
+
+ if (opt.CreateHelpers)
+ generatedAtrr.Add(new MapsterToolGeneratedMapperAttribute(
+ opt.HelpersNamespace ?? Path.GetFileNameWithoutExtension(opt.Assembly)
+ ));
+
+
+ foreach (var type in assembly.GetLoadableTypes())
+ {
+ if (!type.IsInterface)
+ continue;
+ var attr = type.GetCustomAttribute();
+ if (attr == null)
+ continue;
+
+ Console.WriteLine($"Processing: {type.FullName}");
+
+ var segments = GetSegments(type.Namespace, opt.BaseNamespace);
+ var definitions = new TypeDefinitions
+ {
+ Implements = new[] { type },
+ Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
+ TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)),
+ IsInternal = attr.IsInternal,
+ PrintFullTypeName = opt.PrintFullTypeName,
+ GeneratedAttributes = new(generatedAtrr)
+ };
+
+ bool? _isForceInternal = definitions.IsInternal ? true : null;
+
+ var path = GetOutput(opt.Output, segments, definitions.TypeName);
+ if (opt.SkipExistingFiles && File.Exists(path))
+ {
+ Console.WriteLine(
+ $"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists."
+ );
+ continue;
+ }
+
+ var translator = new ExpressionTranslator(definitions);
+ var interfaces = type.GetAllInterfaces();
+ foreach (var @interface in interfaces)
+ {
+ foreach (var prop in @interface.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(x => x.IsGetterPublicOrInternal())
+ )
+ {
+ if (!prop.PropertyType.IsGenericType)
+ continue;
+ if (prop.PropertyType.GetGenericTypeDefinition() != typeof(Expression<>))
+ continue;
+ var propArgs = prop.PropertyType.GetGenericArguments()[0];
+ if (!propArgs.IsGenericType)
+ continue;
+ if (propArgs.GetGenericTypeDefinition() != typeof(Func<,>))
+ continue;
+ var funcArgs = propArgs.GetGenericArguments();
+ var tuple = new TypeTuple(funcArgs[0], funcArgs[1]);
+ var expr = config.CreateMapExpression(tuple, MapType.Projection);
+ translator.VisitLambdaForGenerateMappers(
+ expr,
+ ExpressionTranslator.LambdaType.PublicLambda,
+ @interface,
+ prop.Name,
+ _isForceInternal ?? (!prop.GetMethod?.IsPublic ?? false)
+ );
+ }
+ }
+
+ foreach (var @interface in interfaces)
+ {
+ foreach (var method in @interface.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(x => x.IsPublicOrInternal())
+ )
+ {
+ if (method.IsGenericMethod)
+ continue;
+ if (method.ReturnType == typeof(void))
+ continue;
+ var methodArgs = method.GetParameters();
+ if (methodArgs.Length < 1 || methodArgs.Length > 2)
+ continue;
+ var tuple = new TypeTuple(methodArgs[0].ParameterType, method.ReturnType);
+ var expr = config.CreateMapExpression(
+ tuple,
+ methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget
+ );
+ translator.VisitLambdaForGenerateMappers(
+ expr,
+ ExpressionTranslator.LambdaType.PublicMethod,
+ @interface,
+ method.Name,
+ _isForceInternal ?? !method.IsPublic
+ );
+ }
+ }
+
+ var code = opt.GenerateNullableDirective
+ ? $"#nullable enable{Environment.NewLine}{translator}"
+ : translator.ToString();
+
+ if(DebugMappers != null)
+ DebugMappers.Add(code);
+ else
+ WriteFile(code, path);
+ }
+
+
+ foreach (var item in generatedAtrr)
+ {
+ WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName));
+ }
+ }
+
+ private static string GetImplName(string name)
+ {
+ if (name.Length >= 2 && name[0] == 'I' && name[1] >= 'A' && name[1] <= 'Z')
+ return name.Substring(1);
+ return name + "Impl";
+ }
+
+ internal static void GenerateModels(ModelOptions opt, List? DebugModels = null)
+ {
+ var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
+ assemblyPath: Path.GetFullPath(opt.Assembly),
+ deferToContext: AssemblyLoadContext.Default,
+ typeof(MapperAttribute).Assembly.GetName(),
+ typeof(IRegister).Assembly.GetName()
+ );
+ var codeGenConfig = new CodeGenerationConfig();
+ codeGenConfig.Scan(assembly);
+
+ var types = assembly.GetLoadableTypes().ToHashSet();
+ foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
+ {
+ foreach (var setting in builder.TypeSettings)
+ {
+ types.Add(setting.Key);
+ }
+ }
+ foreach (var type in types)
+ {
+ var builders = type.GetAdaptAttributeBuilders(codeGenConfig)
+ .Where(
+ it =>
+ !string.IsNullOrEmpty(it.Attribute.Name)
+ && it.Attribute.Name != "[name]"
+ )
+ .ToList();
+ if (builders.Count == 0)
+ continue;
+
+ Console.WriteLine($"Processing: {type.FullName}");
+ foreach (var builder in builders)
+ {
+ CreateModel(opt, type, builder, DebugModels);
+ }
+ }
+ }
+
+ private static byte? GetTypeNullableContext(Type type)
+ {
+ var nilCtxAttr = type.GetCustomAttributesData()
+ .FirstOrDefault(it => it.AttributeType.Name == "NullableContextAttribute");
+ return
+ nilCtxAttr?.ConstructorArguments.Count == 1
+ && nilCtxAttr.ConstructorArguments[0].Value is byte b
+ ? (byte?)b
+ : null;
+ }
+
+ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuilder builder, List? DebugModels)
+ {
+ var segments = GetSegments(type.Namespace, opt.BaseNamespace);
+ var attr = builder.Attribute;
+ var definitions = new TypeDefinitions
+ {
+ Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
+ TypeName = attr.Name!.Replace("[name]", type.Name),
+ PrintFullTypeName = opt.PrintFullTypeName,
+ IsRecordType = opt.IsRecordType,
+ NullableContext = GetTypeNullableContext(type),
+ };
+
+ var path = GetOutput(opt.Output, segments, definitions.TypeName);
+ if (opt.SkipExistingFiles && File.Exists(path))
+ {
+ Console.WriteLine(
+ $"Skipped: {type.FullName}. Model {definitions.TypeName} already exists."
+ );
+ return;
+ }
+
+ var translator = new ExpressionTranslator(definitions);
+ var isAdaptTo = attr is AdaptToAttribute;
+ var isTwoWays = attr is AdaptTwoWaysAttribute;
+ var side = isAdaptTo ? MemberSide.Source : MemberSide.Destination;
+ var properties = type.GetFieldsAndProperties()
+ .Where(
+ it =>
+ !it.SafeGetCustomAttributes()
+ .OfType()
+ .Any(it2 => isTwoWays || it2.Side == null || it2.Side == side)
+ );
+
+ if (attr.IgnoreAttributes != null)
+ {
+ properties = properties.Where(
+ it =>
+ !it.SafeGetCustomAttributes()
+ .Select(it2 => it2.GetType())
+ .Intersect(attr.IgnoreAttributes)
+ .Any()
+ );
+ }
+
+ if (attr.IgnoreNoAttributes != null)
+ {
+ properties = properties.Where(
+ it =>
+ it.SafeGetCustomAttributes()
+ .Select(it2 => it2.GetType())
+ .Intersect(attr.IgnoreNoAttributes)
+ .Any()
+ );
+ }
+
+ if (attr.IgnoreNamespaces != null)
+ {
+ foreach (var ns in attr.IgnoreNamespaces)
+ {
+ properties = properties.Where(
+ it => getPropType(it).Namespace?.StartsWith(ns) != true
+ );
+ }
+ }
+
+ var propSettings = builder.TypeSettings.GetValueOrDefault(type);
+ var isReadOnly = isAdaptTo && attr.MapToConstructor;
+ var isNullable = !isAdaptTo && attr.IgnoreNullValues;
+ foreach (var member in properties)
+ {
+ var setting = propSettings?.GetValueOrDefault(member.Name);
+ if (setting?.Ignore == true)
+ continue;
+
+ var adaptMember = member.GetCustomAttribute();
+ if (!isTwoWays && adaptMember?.Side != null && adaptMember.Side != side)
+ adaptMember = null;
+ var propType =
+ setting?.MapFunc?.ReturnType
+ ?? setting?.TargetPropertyType
+ ?? GetPropertyType(
+ member,
+ getPropType(member),
+ attr.GetType(),
+ opt.Namespace,
+ builder
+ );
+ var nilAttr = member
+ .GetCustomAttributesData()
+ .FirstOrDefault(it => it.AttributeType.Name == "NullableAttribute");
+ var nilAttrArg =
+ nilAttr?.ConstructorArguments.Count == 1
+ ? nilAttr.ConstructorArguments[0].Value
+ : null;
+ translator.Properties.Add(
+ new PropertyDefinitions
+ {
+ Name = setting?.TargetPropertyName ?? adaptMember?.Name ?? member.Name,
+ Type = isNullable ? propType.MakeNullable() : propType,
+ IsReadOnly = isReadOnly,
+ NullableContext = nilAttrArg is byte b ? (byte?)b : null,
+ Nullable = nilAttrArg is byte[] bytes ? bytes : null,
+ }
+ );
+ }
+
+ var code = opt.GenerateNullableDirective
+ ? $"#nullable enable{Environment.NewLine}{translator}"
+ : translator.ToString();
+
+ if (DebugModels != null)
+ DebugModels.Add(code);
+ else
+ WriteFile(code, path);
+
+ static Type getPropType(MemberInfo mem)
+ {
+ return mem is PropertyInfo p ? p.PropertyType : ((FieldInfo)mem).FieldType;
+ }
+ }
+
+ private static readonly Dictionary _mockTypes =
+ new Dictionary();
+
+ private static Type GetPropertyType(
+ MemberInfo member,
+ Type propType,
+ Type attrType,
+ string? ns,
+ AdaptAttributeBuilder builder
+ )
+ {
+ var navAttr = member
+ .SafeGetCustomAttributes()
+ .OfType()
+ .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false);
+ if (navAttr != null)
+ return navAttr.Type;
+
+ if (
+ propType.IsCollection()
+ && propType.IsCollectionCompatible()
+ && propType.IsGenericType
+ && propType.GetGenericArguments().Length == 1
+ )
+ {
+ var elementType = propType.GetGenericArguments()[0];
+ var newType = GetPropertyType(member, elementType, attrType, ns, builder);
+ if (elementType == newType)
+ return propType;
+ var generic = propType.GetGenericTypeDefinition();
+ return generic.MakeGenericType(newType);
+ }
+
+ var alterType = builder.AlterTypes
+ .Select(fn => fn(propType))
+ .FirstOrDefault(it => it != null);
+ if (alterType != null)
+ return alterType;
+
+ var propTypeAttrs = propType.SafeGetCustomAttributes();
+ navAttr = propTypeAttrs
+ .OfType()
+ .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false);
+ if (navAttr != null)
+ return navAttr.Type;
+
+ var adaptAttr = builder.TypeSettings.ContainsKey(propType)
+ ? (BaseAdaptAttribute?)builder.Attribute
+ : propTypeAttrs
+ .OfType()
+ .FirstOrDefault(it => it.GetType() == attrType);
+ if (adaptAttr == null)
+ return propType;
+ if (adaptAttr.Type != null)
+ return adaptAttr.Type;
+
+ var name = adaptAttr.Name!.Replace("[name]", propType.Name);
+ if (!_mockTypes.TryGetValue(name, out var mockType))
+ {
+ mockType = new MockType(ns ?? propType.Namespace!, name, propType.Assembly);
+ _mockTypes[name] = mockType;
+ }
+ return mockType;
+ }
+
+ private static Type? GetFromType(Type type, BaseAdaptAttribute attr, HashSet types)
+ {
+ if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute))
+ return null;
+
+ var fromType = attr.Type;
+ if (fromType == null && attr.Name != null)
+ {
+ var name = attr.Name.Replace("[name]", type.Name);
+ fromType = types.FirstOrDefault(it => it.Name == name);
+ }
+
+ return fromType;
+ }
+
+ private static Type? GetToType(Type type, BaseAdaptAttribute attr, HashSet types)
+ {
+ if (!(attr is AdaptToAttribute))
+ return null;
+
+ var toType = attr.Type;
+ if (toType == null && attr.Name != null)
+ {
+ var name = attr.Name.Replace("[name]", type.Name);
+ toType = types.FirstOrDefault(it => it.Name == name);
+ }
+
+ return toType;
+ }
+
+ private static void ApplySettings(
+ TypeAdapterSetter setter,
+ BaseAdaptAttribute attr,
+ Dictionary settings
+ )
+ {
+ setter.ApplyAdaptAttribute(attr);
+ foreach (var (name, setting) in settings)
+ {
+ if (setting.MapFunc != null)
+ {
+ setter.Settings.Resolvers.Add(
+ new InvokerModel
+ {
+ DestinationMemberName = setting.TargetPropertyName ?? name,
+ Invoker = setting.MapFunc,
+ }
+ );
+ }
+ else if (setting.TargetPropertyName != null)
+ {
+ setter.Map(setting.TargetPropertyName, name);
+ }
+ }
+ }
+
+ internal static void GenerateExtensions(ExtensionOptions opt, List? DebugExtentions = null)
+ {
+ var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
+ assemblyPath: Path.GetFullPath(opt.Assembly),
+ deferToContext: AssemblyLoadContext.Default,
+ typeof(MapperAttribute).Assembly.GetName(),
+ typeof(IRegister).Assembly.GetName()
+ );
+ var config = TypeAdapterConfig.GlobalSettings;
+ config.SelfContainedCodeGeneration = true;
+ config.Scan(assembly);
+ var codeGenConfig = new CodeGenerationConfig();
+ codeGenConfig.Scan(assembly);
+
+ var assemblies = new HashSet { assembly };
+ foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
+ {
+ foreach (var setting in builder.TypeSettings)
+ {
+ assemblies.Add(setting.Key.Assembly);
+ }
+ }
+ var types = assemblies.SelectMany(it => it.GetLoadableTypes()).ToHashSet();
+
+ // assemblies defines open generic only, so we have to add specialised types used in mappings
+ foreach (var (key, _) in config.RuleMap)
+ types.Add(key.Source);
+ var configDict = new Dictionary();
+ foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
+ {
+ var attr = builder.Attribute;
+ var cloned = config.Clone();
+ foreach (var (type, settings) in builder.TypeSettings)
+ {
+ var fromType = GetFromType(type, attr, types);
+ if (fromType != null)
+ ApplySettings(cloned.ForType(fromType, type), attr, settings);
+
+ var toType = GetToType(type, attr, types);
+ if (toType != null)
+ ApplySettings(cloned.ForType(type, toType), attr, settings);
+ }
+
+ configDict[attr] = cloned;
+ }
+
+ foreach (var type in types)
+ {
+ var mapperAttr = type.GetGenerateMapperAttributes(codeGenConfig).FirstOrDefault();
+ var ruleMaps = config.RuleMap
+ .Where(
+ it => it.Key.Source == type && it.Value.Settings.GenerateMapper is MapType
+ )
+ .ToList();
+ if (mapperAttr == null && ruleMaps.Count == 0)
+ continue;
+
+ mapperAttr ??= new GenerateMapperAttribute();
+ var set = mapperAttr.ForAttributes?.ToHashSet();
+ var builders = type.GetAdaptAttributeBuilders(codeGenConfig)
+ .Where(it => set?.Contains(it.GetType()) != false)
+ .ToList();
+ if (builders.Count == 0 && ruleMaps.Count == 0)
+ continue;
+
+ Console.WriteLine($"Processing: {type.FullName}");
+
+ var segments = GetSegments(type.Namespace, opt.BaseNamespace);
+ var definitions = new TypeDefinitions
+ {
+ IsStatic = true,
+ Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
+ TypeName = mapperAttr.Name.Replace("[name]", GetCodeFriendlyTypeName(type)),
+ IsInternal = mapperAttr.IsInternal,
+ PrintFullTypeName = opt.PrintFullTypeName,
+ };
+
+ var path = GetOutput(opt.Output, segments, definitions.TypeName);
+ if (opt.SkipExistingFiles && File.Exists(path))
+ {
+ Console.WriteLine(
+ $"Skipped: {type.FullName}. Extension class {definitions.TypeName} already exists."
+ );
+ continue;
+ }
+
+ var translator = new ExpressionTranslator(definitions);
+
+ foreach (var builder in builders)
+ {
+ var attr = builder.Attribute;
+ var cloned = configDict.GetValueOrDefault(attr) ?? config;
+ var fromType = GetFromType(type, attr, types);
+ if (fromType != null)
+ {
+ var tuple = new TypeTuple(fromType, type);
+ var mapType =
+ attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType;
+ GenerateExtensionMethods(
+ mapType,
+ cloned,
+ tuple,
+ translator,
+ type,
+ mapperAttr.IsHelperClass
+ );
+ }
+
+ var toType = GetToType(type, attr, types);
+ if (toType != null && (!(attr is AdaptTwoWaysAttribute) || type != toType))
+ {
+ var tuple = new TypeTuple(type, toType);
+ var mapType =
+ attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType;
+ GenerateExtensionMethods(
+ mapType,
+ cloned,
+ tuple,
+ translator,
+ type,
+ mapperAttr.IsHelperClass
+ );
+ }
+ }
+
+ foreach (var (tuple, rule) in ruleMaps)
+ {
+ var mapType = (MapType)rule.Settings.GenerateMapper!;
+ GenerateExtensionMethods(
+ mapType,
+ config,
+ tuple,
+ translator,
+ type,
+ mapperAttr.IsHelperClass
+ );
+ }
+
+ var code = opt.GenerateNullableDirective
+ ? $"#nullable enable{Environment.NewLine}{translator}"
+ : translator.ToString();
+
+ if(DebugExtentions != null)
+ DebugExtentions.Add(code);
+ else
+ WriteFile(code, path);
+ }
+ }
+
+ private static void GenerateExtensionMethods(
+ MapType mapType,
+ TypeAdapterConfig config,
+ TypeTuple tuple,
+ ExpressionTranslator translator,
+ Type entityType,
+ bool isHelperClass
+ )
+ {
+ //add type name to prevent duplication
+ translator.Translate(entityType);
+ var destName = GetCodeFriendlyTypeName(tuple.Destination);
+
+ var name =
+ tuple.Destination.Name == entityType.Name
+ ? destName
+ : destName.Replace(entityType.Name, "");
+ if ((mapType & MapType.Map) > 0)
+ {
+ var expr = config.CreateMapExpression(tuple, MapType.Map);
+ translator.VisitLambda(
+ expr,
+ isHelperClass
+ ? ExpressionTranslator.LambdaType.PublicMethod
+ : ExpressionTranslator.LambdaType.ExtensionMethod,
+ "AdaptTo" + name
+ );
+ }
+
+ if ((mapType & MapType.MapToTarget) > 0)
+ {
+ var expr2 = config.CreateMapExpression(tuple, MapType.MapToTarget);
+ translator.VisitLambda(
+ expr2,
+ isHelperClass
+ ? ExpressionTranslator.LambdaType.PublicMethod
+ : ExpressionTranslator.LambdaType.ExtensionMethod,
+ "AdaptTo"
+ );
+ }
+
+ if ((mapType & MapType.Projection) > 0)
+ {
+ var proj = config.CreateMapExpression(tuple, MapType.Projection);
+ translator.VisitLambda(
+ proj,
+ ExpressionTranslator.LambdaType.PublicLambda,
+ "ProjectTo" + name
+ );
+ }
+ }
+
+ private static string GetCodeFriendlyTypeName(Type type) =>
+ GetCodeFriendlyTypeName(new StringBuilder(), type).ToString();
+
+ private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type)
+ {
+ foreach (var subType in type.GenericTypeArguments)
+ {
+ GetCodeFriendlyTypeName(sb, subType);
+ }
+
+ if (type.IsArray)
+ {
+ GetCodeFriendlyTypeName(sb, type.GetElementType()!);
+ sb.Append("Array");
+ return sb;
+ }
+
+ var name = type.Name;
+ var i = name.IndexOf('`');
+ if (i > 0)
+ name = name.Remove(i);
+ name = name switch
+ {
+ "SByte" => "Sbyte",
+ "Int16" => "Short",
+ "UInt16" => "Ushort",
+ "Int32" => "Int",
+ "UInt32" => "Uint",
+ "Int64" => "Long",
+ "UInt64" => "Ulong",
+ "Single" => "Float",
+ "Boolean" => "Bool",
+ _ => name,
+ };
+
+ if (!string.IsNullOrEmpty(name))
+ sb.Append(name);
+ return sb;
+ }
+ }
+}
diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs
index e93ce4d4..f49209b3 100644
--- a/src/Mapster.Tool/Program.cs
+++ b/src/Mapster.Tool/Program.cs
@@ -1,21 +1,10 @@
using CommandLine;
-using ExpressionDebugger;
-using ExpressionDebugger.Helpers;
-using ExpressionDebugger.Helpers.GeneratedAttributes;
-using Mapster.Models;
-using Mapster.Utils;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Runtime.Loader;
-using System.Text;
+using System.Runtime.CompilerServices;
+[assembly: InternalsVisibleTo("Mapster.Tool.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd523e79e4decc052a3501363d71ecc123b9ce4bd5a8c949e81bc482d8b6822366ed6aead5ebace01aae3ade49e116fde094af03c34cdbc2ebcb89346ca510fac6246b240b71968ab7f9a24de44d680dc93307f9e8a2b00bec7c523db9696679b56725d622cfb01f4eb2604333a0a0e9f580cd6f5c3d5034b3e66f52d818e9a5")]
namespace Mapster.Tool
{
- class Program
+ internal class Program
{
static void Main(string[] args)
{
@@ -26,716 +15,19 @@ static void Main(string[] args)
.WithParsed(GenerateExtensions);
}
- private static string? GetSegments(string? ns, string? baseNs)
+ private static void GenerateExtensions(ExtensionOptions options)
{
- if (ns == null || string.IsNullOrEmpty(baseNs) || baseNs == ns)
- return null;
- return ns.StartsWith(baseNs + ".") ? ns.Substring(baseNs.Length + 1) : ns;
+ Generators.GenerateExtensions(options);
}
- private static string? CreateNamespace(string? ns, string? segment, string? typeNs)
+ private static void GenerateModels(ModelOptions options)
{
- if (ns == null)
- return typeNs;
- return segment == null ? ns : $"{ns}.{segment}";
+ Generators.GenerateModels(options);
}
- private static string GetOutput(string baseOutput, string? segment, string typeName)
+ private static void GenerateMappers(MapperOptions options)
{
- var fullBasePath = Path.GetFullPath(baseOutput);
- return segment == null
- ? Path.Combine(fullBasePath, typeName + ".g.cs")
- : Path.Combine(
- fullBasePath,
- segment.Replace('.', Path.DirectorySeparatorChar),
- typeName + ".g.cs"
- );
- }
-
- private static void WriteFile(string code, string path)
- {
- var dir = Path.GetDirectoryName(path);
- if (dir != null)
- Directory.CreateDirectory(dir);
- if (File.Exists(path))
- {
- var old = File.ReadAllText(path);
- if (old == code)
- return;
- }
- File.WriteAllText(path, code);
- }
-
- private static void GenerateMappers(MapperOptions opt)
- {
- // We want loaded assemblies that we're scanning to be isolated from our currently
- // running assembly load context in order to avoid type/framework collisions between Mapster assemblies
- // and their dependencies and the scanned assemblies and their dependencies
-
- // However, we also need *some* of those scanned assemblies and thus their types to resolve from our
- // currently running AssemblyLoadContext.Default: The Mapster assembly basically.
-
- // This way when we compare attribute types (such as MapperAttribute) between our running assembly
- // and the scanned assembly the two types with the same FullName can be considered equal because
- // they both were resolved from AssemblyLoadContext.Default.
-
- // This isolated Assembly Load Context will be able to resolve the Mapster assembly, but
- // the resolved Assembly will be the same one that is in AssemblyLoadContext.Default
- // (the runtime assembly load context that our code refers to by default when referencing
- // types)
- var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
- assemblyPath: Path.GetFullPath(opt.Assembly),
- deferToContext: AssemblyLoadContext.Default,
- typeof(MapperAttribute).Assembly.GetName(),
- typeof(IRegister).Assembly.GetName()
- );
- var config = TypeAdapterConfig.GlobalSettings;
- config.SelfContainedCodeGeneration = true;
- config.Scan(assembly);
-
- var generatedAtrr = new List();
-
- if (opt.CreateHelpers)
- generatedAtrr.Add(new MapsterToolGeneratedMapperAttribute(
- opt.HelpersNamespace ?? Path.GetFileNameWithoutExtension(opt.Assembly)
- ));
-
-
- foreach (var type in assembly.GetLoadableTypes())
- {
- if (!type.IsInterface)
- continue;
- var attr = type.GetCustomAttribute();
- if (attr == null)
- continue;
-
- Console.WriteLine($"Processing: {type.FullName}");
-
- var segments = GetSegments(type.Namespace, opt.BaseNamespace);
- var definitions = new TypeDefinitions
- {
- Implements = new[] { type },
- Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
- TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)),
- IsInternal = attr.IsInternal,
- PrintFullTypeName = opt.PrintFullTypeName,
- GeneratedAttributes = new(generatedAtrr)
- };
-
- bool? _isForceInternal = definitions.IsInternal ? true : null;
-
- var path = GetOutput(opt.Output, segments, definitions.TypeName);
- if (opt.SkipExistingFiles && File.Exists(path))
- {
- Console.WriteLine(
- $"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists."
- );
- continue;
- }
-
- var translator = new ExpressionTranslator(definitions);
- var interfaces = type.GetAllInterfaces();
- foreach (var @interface in interfaces)
- {
- foreach (var prop in @interface.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- .Where(x => x.IsGetterPublicOrInternal())
- )
- {
- if (!prop.PropertyType.IsGenericType)
- continue;
- if (prop.PropertyType.GetGenericTypeDefinition() != typeof(Expression<>))
- continue;
- var propArgs = prop.PropertyType.GetGenericArguments()[0];
- if (!propArgs.IsGenericType)
- continue;
- if (propArgs.GetGenericTypeDefinition() != typeof(Func<,>))
- continue;
- var funcArgs = propArgs.GetGenericArguments();
- var tuple = new TypeTuple(funcArgs[0], funcArgs[1]);
- var expr = config.CreateMapExpression(tuple, MapType.Projection);
- translator.VisitLambdaForGenerateMappers(
- expr,
- ExpressionTranslator.LambdaType.PublicLambda,
- @interface,
- prop.Name,
- _isForceInternal ?? (!prop.GetMethod?.IsPublic ?? false)
- );
- }
- }
-
- foreach (var @interface in interfaces)
- {
- foreach (var method in @interface.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- .Where(x => x.IsPublicOrInternal())
- )
- {
- if (method.IsGenericMethod)
- continue;
- if (method.ReturnType == typeof(void))
- continue;
- var methodArgs = method.GetParameters();
- if (methodArgs.Length < 1 || methodArgs.Length > 2)
- continue;
- var tuple = new TypeTuple(methodArgs[0].ParameterType, method.ReturnType);
- var expr = config.CreateMapExpression(
- tuple,
- methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget
- );
- translator.VisitLambdaForGenerateMappers(
- expr,
- ExpressionTranslator.LambdaType.PublicMethod,
- @interface,
- method.Name,
- _isForceInternal ?? !method.IsPublic
- );
- }
- }
-
- var code = opt.GenerateNullableDirective
- ? $"#nullable enable{Environment.NewLine}{translator}"
- : translator.ToString();
- WriteFile(code, path);
- }
-
-
- foreach (var item in generatedAtrr)
- {
- WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName));
- }
- }
-
- private static string GetImplName(string name)
- {
- if (name.Length >= 2 && name[0] == 'I' && name[1] >= 'A' && name[1] <= 'Z')
- return name.Substring(1);
- return name + "Impl";
- }
-
- private static void GenerateModels(ModelOptions opt)
- {
- var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
- assemblyPath: Path.GetFullPath(opt.Assembly),
- deferToContext: AssemblyLoadContext.Default,
- typeof(MapperAttribute).Assembly.GetName(),
- typeof(IRegister).Assembly.GetName()
- );
- var codeGenConfig = new CodeGenerationConfig();
- codeGenConfig.Scan(assembly);
-
- var types = assembly.GetLoadableTypes().ToHashSet();
- foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
- {
- foreach (var setting in builder.TypeSettings)
- {
- types.Add(setting.Key);
- }
- }
- foreach (var type in types)
- {
- var builders = type.GetAdaptAttributeBuilders(codeGenConfig)
- .Where(
- it =>
- !string.IsNullOrEmpty(it.Attribute.Name)
- && it.Attribute.Name != "[name]"
- )
- .ToList();
- if (builders.Count == 0)
- continue;
-
- Console.WriteLine($"Processing: {type.FullName}");
- foreach (var builder in builders)
- {
- CreateModel(opt, type, builder);
- }
- }
- }
-
- private static byte? GetTypeNullableContext(Type type)
- {
- var nilCtxAttr = type.GetCustomAttributesData()
- .FirstOrDefault(it => it.AttributeType.Name == "NullableContextAttribute");
- return
- nilCtxAttr?.ConstructorArguments.Count == 1
- && nilCtxAttr.ConstructorArguments[0].Value is byte b
- ? (byte?)b
- : null;
- }
-
- private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuilder builder)
- {
- var segments = GetSegments(type.Namespace, opt.BaseNamespace);
- var attr = builder.Attribute;
- var definitions = new TypeDefinitions
- {
- Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
- TypeName = attr.Name!.Replace("[name]", type.Name),
- PrintFullTypeName = opt.PrintFullTypeName,
- IsRecordType = opt.IsRecordType,
- NullableContext = GetTypeNullableContext(type),
- };
-
- var path = GetOutput(opt.Output, segments, definitions.TypeName);
- if (opt.SkipExistingFiles && File.Exists(path))
- {
- Console.WriteLine(
- $"Skipped: {type.FullName}. Model {definitions.TypeName} already exists."
- );
- return;
- }
-
- var translator = new ExpressionTranslator(definitions);
- var isAdaptTo = attr is AdaptToAttribute;
- var isTwoWays = attr is AdaptTwoWaysAttribute;
- var side = isAdaptTo ? MemberSide.Source : MemberSide.Destination;
- var properties = type.GetFieldsAndProperties()
- .Where(
- it =>
- !it.SafeGetCustomAttributes()
- .OfType()
- .Any(it2 => isTwoWays || it2.Side == null || it2.Side == side)
- );
-
- if (attr.IgnoreAttributes != null)
- {
- properties = properties.Where(
- it =>
- !it.SafeGetCustomAttributes()
- .Select(it2 => it2.GetType())
- .Intersect(attr.IgnoreAttributes)
- .Any()
- );
- }
-
- if (attr.IgnoreNoAttributes != null)
- {
- properties = properties.Where(
- it =>
- it.SafeGetCustomAttributes()
- .Select(it2 => it2.GetType())
- .Intersect(attr.IgnoreNoAttributes)
- .Any()
- );
- }
-
- if (attr.IgnoreNamespaces != null)
- {
- foreach (var ns in attr.IgnoreNamespaces)
- {
- properties = properties.Where(
- it => getPropType(it).Namespace?.StartsWith(ns) != true
- );
- }
- }
-
- var propSettings = builder.TypeSettings.GetValueOrDefault(type);
- var isReadOnly = isAdaptTo && attr.MapToConstructor;
- var isNullable = !isAdaptTo && attr.IgnoreNullValues;
- foreach (var member in properties)
- {
- var setting = propSettings?.GetValueOrDefault(member.Name);
- if (setting?.Ignore == true)
- continue;
-
- var adaptMember = member.GetCustomAttribute();
- if (!isTwoWays && adaptMember?.Side != null && adaptMember.Side != side)
- adaptMember = null;
- var propType =
- setting?.MapFunc?.ReturnType
- ?? setting?.TargetPropertyType
- ?? GetPropertyType(
- member,
- getPropType(member),
- attr.GetType(),
- opt.Namespace,
- builder
- );
- var nilAttr = member
- .GetCustomAttributesData()
- .FirstOrDefault(it => it.AttributeType.Name == "NullableAttribute");
- var nilAttrArg =
- nilAttr?.ConstructorArguments.Count == 1
- ? nilAttr.ConstructorArguments[0].Value
- : null;
- translator.Properties.Add(
- new PropertyDefinitions
- {
- Name = setting?.TargetPropertyName ?? adaptMember?.Name ?? member.Name,
- Type = isNullable ? propType.MakeNullable() : propType,
- IsReadOnly = isReadOnly,
- NullableContext = nilAttrArg is byte b ? (byte?)b : null,
- Nullable = nilAttrArg is byte[] bytes ? bytes : null,
- }
- );
- }
-
- var code = opt.GenerateNullableDirective
- ? $"#nullable enable{Environment.NewLine}{translator}"
- : translator.ToString();
- WriteFile(code, path);
-
- static Type getPropType(MemberInfo mem)
- {
- return mem is PropertyInfo p ? p.PropertyType : ((FieldInfo)mem).FieldType;
- }
- }
-
- private static readonly Dictionary _mockTypes =
- new Dictionary();
-
- private static Type GetPropertyType(
- MemberInfo member,
- Type propType,
- Type attrType,
- string? ns,
- AdaptAttributeBuilder builder
- )
- {
- var navAttr = member
- .SafeGetCustomAttributes()
- .OfType()
- .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false);
- if (navAttr != null)
- return navAttr.Type;
-
- if (
- propType.IsCollection()
- && propType.IsCollectionCompatible()
- && propType.IsGenericType
- && propType.GetGenericArguments().Length == 1
- )
- {
- var elementType = propType.GetGenericArguments()[0];
- var newType = GetPropertyType(member, elementType, attrType, ns, builder);
- if (elementType == newType)
- return propType;
- var generic = propType.GetGenericTypeDefinition();
- return generic.MakeGenericType(newType);
- }
-
- var alterType = builder.AlterTypes
- .Select(fn => fn(propType))
- .FirstOrDefault(it => it != null);
- if (alterType != null)
- return alterType;
-
- var propTypeAttrs = propType.SafeGetCustomAttributes();
- navAttr = propTypeAttrs
- .OfType()
- .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false);
- if (navAttr != null)
- return navAttr.Type;
-
- var adaptAttr = builder.TypeSettings.ContainsKey(propType)
- ? (BaseAdaptAttribute?)builder.Attribute
- : propTypeAttrs
- .OfType()
- .FirstOrDefault(it => it.GetType() == attrType);
- if (adaptAttr == null)
- return propType;
- if (adaptAttr.Type != null)
- return adaptAttr.Type;
-
- var name = adaptAttr.Name!.Replace("[name]", propType.Name);
- if (!_mockTypes.TryGetValue(name, out var mockType))
- {
- mockType = new MockType(ns ?? propType.Namespace!, name, propType.Assembly);
- _mockTypes[name] = mockType;
- }
- return mockType;
- }
-
- private static Type? GetFromType(Type type, BaseAdaptAttribute attr, HashSet types)
- {
- if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute))
- return null;
-
- var fromType = attr.Type;
- if (fromType == null && attr.Name != null)
- {
- var name = attr.Name.Replace("[name]", type.Name);
- fromType = types.FirstOrDefault(it => it.Name == name);
- }
-
- return fromType;
- }
-
- private static Type? GetToType(Type type, BaseAdaptAttribute attr, HashSet types)
- {
- if (!(attr is AdaptToAttribute))
- return null;
-
- var toType = attr.Type;
- if (toType == null && attr.Name != null)
- {
- var name = attr.Name.Replace("[name]", type.Name);
- toType = types.FirstOrDefault(it => it.Name == name);
- }
-
- return toType;
- }
-
- private static void ApplySettings(
- TypeAdapterSetter setter,
- BaseAdaptAttribute attr,
- Dictionary settings
- )
- {
- setter.ApplyAdaptAttribute(attr);
- foreach (var (name, setting) in settings)
- {
- if (setting.MapFunc != null)
- {
- setter.Settings.Resolvers.Add(
- new InvokerModel
- {
- DestinationMemberName = setting.TargetPropertyName ?? name,
- SourceMemberName = name,
- Invoker = setting.MapFunc,
- }
- );
- }
- else if (setting.TargetPropertyName != null)
- {
- setter.Map(setting.TargetPropertyName, name);
- }
- }
- }
-
- private static void GenerateExtensions(ExtensionOptions opt)
- {
- var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom(
- assemblyPath: Path.GetFullPath(opt.Assembly),
- deferToContext: AssemblyLoadContext.Default,
- typeof(MapperAttribute).Assembly.GetName(),
- typeof(IRegister).Assembly.GetName()
- );
- var config = TypeAdapterConfig.GlobalSettings;
- config.SelfContainedCodeGeneration = true;
- config.Scan(assembly);
- var codeGenConfig = new CodeGenerationConfig();
- codeGenConfig.Scan(assembly);
-
- var assemblies = new HashSet { assembly };
- foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
- {
- foreach (var setting in builder.TypeSettings)
- {
- assemblies.Add(setting.Key.Assembly);
- }
- }
- var types = assemblies.SelectMany(it => it.GetLoadableTypes()).ToHashSet();
-
- // assemblies defines open generic only, so we have to add specialised types used in mappings
- foreach (var (key, _) in config.RuleMap)
- types.Add(key.Source);
- var configDict = new Dictionary();
- foreach (var builder in codeGenConfig.AdaptAttributeBuilders)
- {
- var attr = builder.Attribute;
- var cloned = config.Clone();
- foreach (var (type, settings) in builder.TypeSettings)
- {
- var fromType = GetFromType(type, attr, types);
- if (fromType != null)
- ApplySettings(cloned.ForType(fromType, type), attr, settings);
-
- var toType = GetToType(type, attr, types);
- if (toType != null)
- ApplySettings(cloned.ForType(type, toType), attr, settings);
- }
-
- configDict[attr] = cloned;
- }
-
- foreach (var type in types)
- {
- var mapperAttr = type.GetGenerateMapperAttributes(codeGenConfig).FirstOrDefault();
- var ruleMaps = config.RuleMap
- .Where(
- it => it.Key.Source == type && it.Value.Settings.GenerateMapper is MapType
- )
- .ToList();
- if (mapperAttr == null && ruleMaps.Count == 0)
- continue;
-
- mapperAttr ??= new GenerateMapperAttribute();
- var set = mapperAttr.ForAttributes?.ToHashSet();
- var builders = type.GetAdaptAttributeBuilders(codeGenConfig)
- .Where(it => set?.Contains(it.GetType()) != false)
- .ToList();
- if (builders.Count == 0 && ruleMaps.Count == 0)
- continue;
-
- Console.WriteLine($"Processing: {type.FullName}");
-
- var segments = GetSegments(type.Namespace, opt.BaseNamespace);
- var definitions = new TypeDefinitions
- {
- IsStatic = true,
- Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace),
- TypeName = mapperAttr.Name.Replace("[name]", GetCodeFriendlyTypeName(type)),
- IsInternal = mapperAttr.IsInternal,
- PrintFullTypeName = opt.PrintFullTypeName,
- };
-
- var path = GetOutput(opt.Output, segments, definitions.TypeName);
- if (opt.SkipExistingFiles && File.Exists(path))
- {
- Console.WriteLine(
- $"Skipped: {type.FullName}. Extension class {definitions.TypeName} already exists."
- );
- continue;
- }
-
- var translator = new ExpressionTranslator(definitions);
-
- foreach (var builder in builders)
- {
- var attr = builder.Attribute;
- var cloned = configDict.GetValueOrDefault(attr) ?? config;
- var fromType = GetFromType(type, attr, types);
- if (fromType != null)
- {
- var tuple = new TypeTuple(fromType, type);
- var mapType =
- attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType;
- GenerateExtensionMethods(
- mapType,
- cloned,
- tuple,
- translator,
- type,
- mapperAttr.IsHelperClass
- );
- }
-
- var toType = GetToType(type, attr, types);
- if (toType != null && (!(attr is AdaptTwoWaysAttribute) || type != toType))
- {
- var tuple = new TypeTuple(type, toType);
- var mapType =
- attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType;
- GenerateExtensionMethods(
- mapType,
- cloned,
- tuple,
- translator,
- type,
- mapperAttr.IsHelperClass
- );
- }
- }
-
- foreach (var (tuple, rule) in ruleMaps)
- {
- var mapType = (MapType)rule.Settings.GenerateMapper!;
- GenerateExtensionMethods(
- mapType,
- config,
- tuple,
- translator,
- type,
- mapperAttr.IsHelperClass
- );
- }
-
- var code = opt.GenerateNullableDirective
- ? $"#nullable enable{Environment.NewLine}{translator}"
- : translator.ToString();
- WriteFile(code, path);
- }
- }
-
- private static void GenerateExtensionMethods(
- MapType mapType,
- TypeAdapterConfig config,
- TypeTuple tuple,
- ExpressionTranslator translator,
- Type entityType,
- bool isHelperClass
- )
- {
- //add type name to prevent duplication
- translator.Translate(entityType);
- var destName = GetCodeFriendlyTypeName(tuple.Destination);
-
- var name =
- tuple.Destination.Name == entityType.Name
- ? destName
- : destName.Replace(entityType.Name, "");
- if ((mapType & MapType.Map) > 0)
- {
- var expr = config.CreateMapExpression(tuple, MapType.Map);
- translator.VisitLambda(
- expr,
- isHelperClass
- ? ExpressionTranslator.LambdaType.PublicMethod
- : ExpressionTranslator.LambdaType.ExtensionMethod,
- "AdaptTo" + name
- );
- }
-
- if ((mapType & MapType.MapToTarget) > 0)
- {
- var expr2 = config.CreateMapExpression(tuple, MapType.MapToTarget);
- translator.VisitLambda(
- expr2,
- isHelperClass
- ? ExpressionTranslator.LambdaType.PublicMethod
- : ExpressionTranslator.LambdaType.ExtensionMethod,
- "AdaptTo"
- );
- }
-
- if ((mapType & MapType.Projection) > 0)
- {
- var proj = config.CreateMapExpression(tuple, MapType.Projection);
- translator.VisitLambda(
- proj,
- ExpressionTranslator.LambdaType.PublicLambda,
- "ProjectTo" + name
- );
- }
- }
-
- private static string GetCodeFriendlyTypeName(Type type) =>
- GetCodeFriendlyTypeName(new StringBuilder(), type).ToString();
-
- private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type)
- {
- foreach (var subType in type.GenericTypeArguments)
- {
- GetCodeFriendlyTypeName(sb, subType);
- }
-
- if (type.IsArray)
- {
- GetCodeFriendlyTypeName(sb, type.GetElementType()!);
- sb.Append("Array");
- return sb;
- }
-
- var name = type.Name;
- var i = name.IndexOf('`');
- if (i > 0)
- name = name.Remove(i);
- name = name switch
- {
- "SByte" => "Sbyte",
- "Int16" => "Short",
- "UInt16" => "Ushort",
- "Int32" => "Int",
- "UInt32" => "Uint",
- "Int64" => "Long",
- "UInt64" => "Ulong",
- "Single" => "Float",
- "Boolean" => "Bool",
- _ => name,
- };
-
- if (!string.IsNullOrEmpty(name))
- sb.Append(name);
- return sb;
+ Generators.GenerateMappers(options);
}
}
}