From 1f9f89c1e3b1765cd06cee5fcd3a5b44794fac52 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 19 Aug 2026 13:26:39 +0500 Subject: [PATCH 1/7] feat: Improvements for MapsterTool GenerateMappers Create Explicit implementation from Mappers interfaces --- .../ExpressionTranslator.cs | 75 +++++++++++++++++++ src/Mapster.Tool/Program.cs | 6 +- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/ExpressionTranslator/ExpressionTranslator.cs b/src/ExpressionTranslator/ExpressionTranslator.cs index 6c219f86..7e851221 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.cs +++ b/src/ExpressionTranslator/ExpressionTranslator.cs @@ -1268,6 +1268,81 @@ public Expression VisitLambda(LambdaExpression node, LambdaType type, string? me } } + public Expression VisitLambdaForGenerateMappers(LambdaExpression node, LambdaType type, Type InterfaceType, string? methodName = null, + bool isInternal = false) + { + VisitLambda(node, type, methodName, isInternal); + + if (type == LambdaType.PrivateLambda || type == LambdaType.PublicLambda) + { + _inlineCount++; + if (type == LambdaType.PublicLambda) + { + var name = methodName != null ? $"{InterfaceType.FullName}.{methodName}" : "Main"; + WriteLine(); + var funcType = MakeDelegateType(node.ReturnType, node.Parameters.Select(it => it.Type).ToArray()); + var exprType = typeof(Expression<>).MakeGenericType(funcType); + Write(Translate(exprType), " ", name, " => "); + } + + IList args; + if (node.Parameters.Count == 1) + { + args = new List(); + var arg = VisitParameter(node.Parameters[0]); + args.Add((ParameterExpression)arg); + } + else + { + args = VisitArguments("(", node.Parameters.ToList(), p => (ParameterExpression)VisitParameter(p), + ")"); + } + + Write(" => "); + var body = VisitGroup(node.Body, ExpressionType.Quote); + if (type == LambdaType.PublicLambda) + Write(";"); + _inlineCount--; + return Expression.Lambda(body, node.Name, node.TailCall, args); + } + else + { + var name = methodName != null ? $"{InterfaceType.FullName}.{methodName}" : "Main"; + if (type == LambdaType.PublicMethod || type == LambdaType.ExtensionMethod) + { + if (!isInternal) + isInternal = node.ReturnType.GetTypeInfo().IsNotPublic || + node.Parameters.Any(it => it.Type.GetTypeInfo().IsNotPublic); + WriteLine(); + Methods[name] = node.Type; + } + else + { + name = GetName(node, name); + WriteModifierNextLine("private"); + } + + Write(Translate(node.ReturnType), " ", name); + var open = "("; + if (type == LambdaType.ExtensionMethod) + { + if (Definitions?.IsStatic != true) + throw new InvalidOperationException("Extension method requires static class"); + if (node.Parameters.Count == 0) + throw new InvalidOperationException("Extension method requires at least 1 parameter"); + open = "(this "; + } + + var args = VisitArguments(open, node.Parameters, VisitParameterDeclaration, ")"); + Indent(); + var body = VisitBody(node.Body, true); + + Outdent(); + + return Expression.Lambda(body, name, node.TailCall, args); + } + } + private HashSet? _visitedLambda; private int _writerLevel; diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 1347cb13..5e0dacec 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -138,9 +138,10 @@ private static void GenerateMappers(MapperOptions opt) var funcArgs = propArgs.GetGenericArguments(); var tuple = new TypeTuple(funcArgs[0], funcArgs[1]); var expr = config.CreateMapExpression(tuple, MapType.Projection); - translator.VisitLambda( + translator.VisitLambdaForGenerateMappers( expr, ExpressionTranslator.LambdaType.PublicLambda, + @interface, prop.Name ); } @@ -162,9 +163,10 @@ private static void GenerateMappers(MapperOptions opt) tuple, methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget ); - translator.VisitLambda( + translator.VisitLambdaForGenerateMappers( expr, ExpressionTranslator.LambdaType.PublicMethod, + @interface, method.Name ); } From de497a0c7327e8e58d61559f56b6864028f1395e Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 19 Aug 2026 13:45:55 +0500 Subject: [PATCH 2/7] feat: add GeneratedMapperAttribute and test --- .../ExpressionTranslator.cs | 14 +++- .../IGeneratedAttribute.cs | 11 +++ .../MapsterToolGeneratedMapperAttribute.cs | 14 ++++ .../Helpers/GeneratedBase.cs | 18 ++++ src/ExpressionTranslator/TypeDefinitions.cs | 4 +- src/Mapster.Tool/Program.cs | 19 +++-- src/TemplateTest/CreateMapExpressionTest.cs | 84 +++++++++++++++++++ 7 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 src/ExpressionTranslator/Helpers/GeneratedAttributes/IGeneratedAttribute.cs create mode 100644 src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs create mode 100644 src/ExpressionTranslator/Helpers/GeneratedBase.cs diff --git a/src/ExpressionTranslator/ExpressionTranslator.cs b/src/ExpressionTranslator/ExpressionTranslator.cs index 7e851221..e7ed9838 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.cs +++ b/src/ExpressionTranslator/ExpressionTranslator.cs @@ -1940,9 +1940,16 @@ public override string ToString() WriteNextLine("using ", ns, ";"); } - WriteLine(); } + foreach (var ns in Definitions.GeneratedAttributes.Select(x => x.NameSpace).Distinct()) + { + WriteNextLine("using ", ns, ";"); + } + + if(_usings != null || Definitions.GeneratedAttributes.Count != 0) + WriteLine(); + // NOTE: type alias cannot solve all name conflicted case, user should use PrintFullTypeName // keep logic here for compatibility if (_typeNames != null) @@ -1966,6 +1973,11 @@ public override string ToString() Indent(); } + foreach (var gAttr in Definitions.GeneratedAttributes) + { + WriteNextLine(gAttr.Implimentation); + } + var isInternal = Definitions.IsInternal; if (!isInternal) isInternal = Definitions.Implements?.Any(it => diff --git a/src/ExpressionTranslator/Helpers/GeneratedAttributes/IGeneratedAttribute.cs b/src/ExpressionTranslator/Helpers/GeneratedAttributes/IGeneratedAttribute.cs new file mode 100644 index 00000000..87803168 --- /dev/null +++ b/src/ExpressionTranslator/Helpers/GeneratedAttributes/IGeneratedAttribute.cs @@ -0,0 +1,11 @@ +namespace ExpressionDebugger.Helpers.GeneratedAttributes +{ + public interface IGeneratedAttribute + { + public string NameSpace { get;} + public string Declaration { get;} + public string Implimentation { get; } + public string FileName { get;} + + } +} diff --git a/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs new file mode 100644 index 00000000..2aed3b57 --- /dev/null +++ b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs @@ -0,0 +1,14 @@ +namespace ExpressionDebugger.Helpers.GeneratedAttributes +{ + public class MapsterToolGeneratedMapperAttribute : GeneratedBase, IGeneratedAttribute + { + public string NameSpace => "Mapster.Generated.Attributes"; + + public string Declaration => + "using System;\r\n\r\nnamespace Mapster.Generated.Attributes\r\n{\r\n public sealed class MapsterToolGeneratedMapperAttribute : Attribute\r\n {\r\n\r\n }\r\n} "; + + public string Implimentation => "[MapsterToolGeneratedMapper]"; + + public string FileName => "MapsterToolGeneratedMapperAttribute"; + } +} diff --git a/src/ExpressionTranslator/Helpers/GeneratedBase.cs b/src/ExpressionTranslator/Helpers/GeneratedBase.cs new file mode 100644 index 00000000..4be95395 --- /dev/null +++ b/src/ExpressionTranslator/Helpers/GeneratedBase.cs @@ -0,0 +1,18 @@ +namespace ExpressionDebugger.Helpers +{ + public abstract class GeneratedBase + { + public override bool Equals(object obj) + { + if(obj is null) + return base.Equals(obj); + else + return this.GetType() == obj.GetType(); + } + + public override int GetHashCode() + { + return this.GetType().GetHashCode(); + } + } +} diff --git a/src/ExpressionTranslator/TypeDefinitions.cs b/src/ExpressionTranslator/TypeDefinitions.cs index a8e20667..ec730304 100644 --- a/src/ExpressionTranslator/TypeDefinitions.cs +++ b/src/ExpressionTranslator/TypeDefinitions.cs @@ -1,4 +1,5 @@ -using System; +using ExpressionDebugger.Helpers.GeneratedAttributes; +using System; using System.Collections.Generic; namespace ExpressionDebugger @@ -12,6 +13,7 @@ public class TypeDefinitions public IEnumerable? Implements { get; set; } public bool PrintFullTypeName { get; set; } public bool IsRecordType { get; set; } + public HashSet GeneratedAttributes { get; set; } = new HashSet(); /// /// Set to 2 to mark all properties as nullable diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 5e0dacec..a456f2d3 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -1,4 +1,9 @@ -using System; +using CommandLine; +using ExpressionDebugger; +using ExpressionDebugger.Helpers.GeneratedAttributes; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,10 +11,6 @@ using System.Reflection; using System.Runtime.Loader; using System.Text; -using CommandLine; -using ExpressionDebugger; -using Mapster.Models; -using Mapster.Utils; namespace Mapster.Tool { @@ -91,6 +92,8 @@ private static void GenerateMappers(MapperOptions opt) config.SelfContainedCodeGeneration = true; config.Scan(assembly); + var generatedAtrr = new[] { new MapsterToolGeneratedMapperAttribute() }; + foreach (var type in assembly.GetLoadableTypes()) { if (!type.IsInterface) @@ -109,6 +112,7 @@ private static void GenerateMappers(MapperOptions opt) TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)), IsInternal = attr.IsInternal, PrintFullTypeName = opt.PrintFullTypeName, + GeneratedAttributes = new(generatedAtrr) }; var path = GetOutput(opt.Output, segments, definitions.TypeName); @@ -177,6 +181,11 @@ private static void GenerateMappers(MapperOptions opt) : 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) diff --git a/src/TemplateTest/CreateMapExpressionTest.cs b/src/TemplateTest/CreateMapExpressionTest.cs index 4929bb12..b191b25e 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -1,7 +1,11 @@ using ExpressionDebugger; +using ExpressionDebugger.Helpers.GeneratedAttributes; using Mapster; +using Mapster.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Collections.Generic; +using System.Linq.Expressions; namespace TemplateTest { @@ -64,6 +68,86 @@ public void TestCreateProjectionExpression() Assert.IsNotNull(code); } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/399 + /// + [TestMethod] + public void TestRegressionMapperGenerationTranslation() + { + var config = new TypeAdapterConfig(); + config.SelfContainedCodeGeneration = true; + + var definitions = new TypeDefinitions + { + Implements = new[] { typeof(IMyTypeMapper) }, + Namespace = "Benchmark", + TypeName = "CustomerMapper", + IsInternal = true, + GeneratedAttributes = new(new[] {new MapsterToolGeneratedMapperAttribute()}) + }; + + var translator = new ExpressionTranslator(definitions); + + foreach (var method in typeof(IMyTypeMapper).GetMethods()) + { + 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, + typeof(IMyTypeMapper), + method.Name + ); + } + + foreach (var prop in typeof(IMyTypeMapper).GetProperties()) + { + 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, + typeof(IMyTypeMapper), + prop.Name + ); + } + + + var txt = translator.ToString(); + + Assert.IsTrue(txt.Contains("Expression> TemplateTest.IMyTypeMapper.Projection")); + Assert.IsTrue(txt.Contains("AddressDTO TemplateTest.IMyTypeMapper.Map")); + Assert.IsTrue(txt.Contains("[MapsterToolGeneratedMapper]")); + + } + + } + + internal interface IMyTypeMapper + { + AddressDTO Map(Address p1); + Expression> Projection { get; } } public class Address From efb4dddbe84620f4c02d663b7f912b35b4a4542c Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 19 Aug 2026 17:33:51 +0500 Subject: [PATCH 3/7] feat: add helpers file created param --- src/Mapster.Tool/MapperOptions.cs | 3 +++ src/Mapster.Tool/Program.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Mapster.Tool/MapperOptions.cs b/src/Mapster.Tool/MapperOptions.cs index 9cd8a1aa..f4725a9b 100644 --- a/src/Mapster.Tool/MapperOptions.cs +++ b/src/Mapster.Tool/MapperOptions.cs @@ -28,6 +28,9 @@ public class MapperOptions [Option('N', "nullableDirective", Required = false, HelpText = "Set true to add \"#nullable enable\" to the top of generated mapper files")] public bool GenerateNullableDirective { get; set; } + [Option('H', "createHelpers", Required = false, HelpText = "Set true to create helper files")] + public bool CreateHelpers { get; set; } + [Usage(ApplicationAlias = "dotnet mapster mapper")] public static IEnumerable Examples => new List diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index a456f2d3..b20ae4c9 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -112,9 +112,12 @@ private static void GenerateMappers(MapperOptions opt) TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)), IsInternal = attr.IsInternal, PrintFullTypeName = opt.PrintFullTypeName, - GeneratedAttributes = new(generatedAtrr) + }; + if (opt.CreateHelpers) + definitions.GeneratedAttributes = new(generatedAtrr); + var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { @@ -182,9 +185,12 @@ private static void GenerateMappers(MapperOptions opt) WriteFile(code, path); } - foreach (var item in generatedAtrr) + if (opt.CreateHelpers) { - WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName)); + foreach (var item in generatedAtrr) + { + WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName)); + } } } From 6312229fc4b098e189d05ce5a4c0092fc93db74f Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 19 Aug 2026 19:35:50 +0500 Subject: [PATCH 4/7] feat: add random --- .../MapsterToolGeneratedMapperAttribute.cs | 30 ++++++++-- .../Helpers/RandomNamespaceGenerator.cs | 56 +++++++++++++++++++ src/TemplateTest/CreateMapExpressionTest.cs | 2 + 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 src/ExpressionTranslator/Helpers/RandomNamespaceGenerator.cs diff --git a/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs index 2aed3b57..18ea2e97 100644 --- a/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs +++ b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs @@ -1,14 +1,36 @@ -namespace ExpressionDebugger.Helpers.GeneratedAttributes +using System.Text; + +namespace ExpressionDebugger.Helpers.GeneratedAttributes { public class MapsterToolGeneratedMapperAttribute : GeneratedBase, IGeneratedAttribute { - public string NameSpace => "Mapster.Generated.Attributes"; + private readonly StringBuilder _Declaration; + private readonly bool _isRandomNameSpace; + private readonly string _NameSpace; + + public string NameSpace => _NameSpace; - public string Declaration => - "using System;\r\n\r\nnamespace Mapster.Generated.Attributes\r\n{\r\n public sealed class MapsterToolGeneratedMapperAttribute : Attribute\r\n {\r\n\r\n }\r\n} "; + public string Declaration => _Declaration.ToString(); public string Implimentation => "[MapsterToolGeneratedMapper]"; public string FileName => "MapsterToolGeneratedMapperAttribute"; + + public MapsterToolGeneratedMapperAttribute(bool isRandomNameSpace = false) + { + _isRandomNameSpace = isRandomNameSpace; + + if (_isRandomNameSpace) + _NameSpace = $"Mapster.Generated.Attributes.{RandomNamespaceGenerator.Generate(1,1)}"; + else + _NameSpace = "Mapster.Generated.Attributes"; + + _Declaration = new StringBuilder(); + + _Declaration.Append("using System;\r\n\r\n"); + _Declaration.Append($"namespace {NameSpace}"); + _Declaration.Append("\r\n{\r\n public sealed class MapsterToolGeneratedMapperAttribute : Attribute\r\n {\r\n\r\n }\r\n} "); + } + } } diff --git a/src/ExpressionTranslator/Helpers/RandomNamespaceGenerator.cs b/src/ExpressionTranslator/Helpers/RandomNamespaceGenerator.cs new file mode 100644 index 00000000..4548a765 --- /dev/null +++ b/src/ExpressionTranslator/Helpers/RandomNamespaceGenerator.cs @@ -0,0 +1,56 @@ +using System; +using System.Text; + +namespace ExpressionDebugger.Helpers +{ + public static class RandomNamespaceGenerator + { + private static readonly Random _random = new Random(); + private const string Consonants = "bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ"; + private const string Vowels = "aeiouAEIOU"; + private const string Digits = "0123456789"; + + public static string Generate(int minParts = 2, int maxParts = 4) + { + if (minParts < 1) minParts = 1; + if (maxParts < minParts) maxParts = minParts; + + int partsCount = _random.Next(minParts, maxParts + 1); + var sb = new StringBuilder(); + + for (int i = 0; i < partsCount; i++) + { + if (i > 0) sb.Append('.'); + sb.Append(GeneratePart()); + } + + return sb.ToString(); + } + + private static string GeneratePart(int minLength = 2, int maxLength = 10) + { + if (minLength < 1) minLength = 1; + if (maxLength < minLength) maxLength = minLength; + + int length = _random.Next(minLength, maxLength + 1); + var sb = new StringBuilder(length); + + sb.Append(Consonants[_random.Next(Consonants.Length)]); + + for (int i = 1; i < length; i++) + { + string pool = (i % 2 == 0) ? Vowels : Consonants; + + if (_random.NextDouble() < 0.1) + { + pool = Digits; + } + + sb.Append(pool[_random.Next(pool.Length)]); + } + + return sb.ToString(); + } + } +} + diff --git a/src/TemplateTest/CreateMapExpressionTest.cs b/src/TemplateTest/CreateMapExpressionTest.cs index b191b25e..514229aa 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -75,6 +75,8 @@ public void TestCreateProjectionExpression() [TestMethod] public void TestRegressionMapperGenerationTranslation() { + var S = new MapsterToolGeneratedMapperAttribute(true); + var config = new TypeAdapterConfig(); config.SelfContainedCodeGeneration = true; From 2a7509cdea7c3f93aaeed633456b0798f75646d4 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Thu, 20 Aug 2026 21:19:24 +0500 Subject: [PATCH 5/7] feat: add supported generate mappers for internal interfaces and public interfaces with internal member and test refactoring --- .../Helpers/MemberInfoExtensions.cs | 33 +++ src/Mapster.Tool/Program.cs | 17 +- src/TemplateTest/CreateMapExpressionTest.cs | 188 +++++++++++++----- 3 files changed, 184 insertions(+), 54 deletions(-) create mode 100644 src/ExpressionTranslator/Helpers/MemberInfoExtensions.cs diff --git a/src/ExpressionTranslator/Helpers/MemberInfoExtensions.cs b/src/ExpressionTranslator/Helpers/MemberInfoExtensions.cs new file mode 100644 index 00000000..b3a1782c --- /dev/null +++ b/src/ExpressionTranslator/Helpers/MemberInfoExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; + +namespace ExpressionDebugger.Helpers +{ + public static class MemberInfoExtensions + { + public static bool IsPublicOrInternal(this MethodInfo method) + { + if (method == null) throw new ArgumentNullException(nameof(method)); + + return !method.IsPrivate + && !method.IsFamily + && !method.IsFamilyOrAssembly + && !method.IsFamilyAndAssembly + && (method.IsPublic || true); + } + + + + public static bool IsGetterPublicOrInternal(this PropertyInfo property) + { + if (property == null) throw new ArgumentNullException(nameof(property)); + + MethodInfo? getMethod = property.GetMethod; + + if (getMethod == null) return false; + + return getMethod.IsPublicOrInternal(); + } + } + +} diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index b20ae4c9..79534284 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -1,5 +1,6 @@ using CommandLine; using ExpressionDebugger; +using ExpressionDebugger.Helpers; using ExpressionDebugger.Helpers.GeneratedAttributes; using Mapster.Models; using Mapster.Utils; @@ -118,6 +119,8 @@ private static void GenerateMappers(MapperOptions opt) if (opt.CreateHelpers) definitions.GeneratedAttributes = new(generatedAtrr); + bool? _isForceInternal = definitions.IsInternal ? true : null; + var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { @@ -131,7 +134,9 @@ private static void GenerateMappers(MapperOptions opt) var interfaces = type.GetAllInterfaces(); foreach (var @interface in interfaces) { - foreach (var prop in @interface.GetProperties()) + foreach (var prop in @interface.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(x => x.IsGetterPublicOrInternal()) + ) { if (!prop.PropertyType.IsGenericType) continue; @@ -149,14 +154,17 @@ private static void GenerateMappers(MapperOptions opt) expr, ExpressionTranslator.LambdaType.PublicLambda, @interface, - prop.Name + prop.Name, + _isForceInternal ?? (!prop.GetMethod?.IsPublic ?? false) ); } } foreach (var @interface in interfaces) { - foreach (var method in @interface.GetMethods()) + foreach (var method in @interface.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(x => x.IsPublicOrInternal()) + ) { if (method.IsGenericMethod) continue; @@ -174,7 +182,8 @@ private static void GenerateMappers(MapperOptions opt) expr, ExpressionTranslator.LambdaType.PublicMethod, @interface, - method.Name + method.Name, + _isForceInternal ?? !method.IsPublic ); } } diff --git a/src/TemplateTest/CreateMapExpressionTest.cs b/src/TemplateTest/CreateMapExpressionTest.cs index 514229aa..928adc32 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -1,11 +1,14 @@ using ExpressionDebugger; +using ExpressionDebugger.Helpers; using ExpressionDebugger.Helpers.GeneratedAttributes; using Mapster; using Mapster.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace TemplateTest { @@ -82,74 +85,95 @@ public void TestRegressionMapperGenerationTranslation() var definitions = new TypeDefinitions { - Implements = new[] { typeof(IMyTypeMapper) }, + Implements = new[] { typeof(IMyTypeMapper), typeof(IMyTypeMapperIntenal) }, Namespace = "Benchmark", TypeName = "CustomerMapper", - IsInternal = true, + IsInternal = false, GeneratedAttributes = new(new[] {new MapsterToolGeneratedMapperAttribute()}) }; var translator = new ExpressionTranslator(definitions); - foreach (var method in typeof(IMyTypeMapper).GetMethods()) - { - 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, - typeof(IMyTypeMapper), - method.Name - ); - } + translator.CreateFromInterface(definitions, config); + + var code = translator.ToString(); + + Assert.IsTrue(code.Contains("public partial class CustomerMapper")); // mapper class is public + + Assert.IsTrue(code.Contains("Expression> TemplateTest.IMyTypeMapper.Projection")); + Assert.IsTrue(code.Contains("AddressDTO TemplateTest.IMyTypeMapper.Map")); + Assert.IsTrue(code.Contains("[MapsterToolGeneratedMapper]")); + + Assert.IsTrue(code.Contains("internal AddressDTO Map")); // create internal method in public interface + + // create as internal because declarate in internal interface and using internal type AddressInternal + Assert.IsTrue(code.Contains("internal AddressInternal MapInternal")); + Assert.IsTrue(code.Contains("internal Expression> ProjectionInternal")); + + + Assert.IsTrue(code.Contains("public AddressDTO MapPublicClassInInternalInterface")); // create public method in internal interface because using public types + + // method using public types in internal interface but marked as internal create as internal method + Assert.IsTrue(code.Contains("internal AddressDTO MapPublicClassInInternalInterfaceWithMarkInternal")); + } + + [TestMethod] + public void CreateForceInternalMapper() + { + var config = new TypeAdapterConfig(); + config.SelfContainedCodeGeneration = true; - foreach (var prop in typeof(IMyTypeMapper).GetProperties()) + var definitions = new TypeDefinitions { - 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, - typeof(IMyTypeMapper), - prop.Name - ); - } - + Implements = new[] { typeof(IMyTypeMapperForce)}, + Namespace = "Benchmark", + TypeName = "CustomerMapper", + IsInternal = true, // force create internal mapper + GeneratedAttributes = new(new[] { new MapsterToolGeneratedMapperAttribute() }) + }; + + var translator = new ExpressionTranslator(definitions); + + translator.CreateFromInterface(definitions, config); - var txt = translator.ToString(); + var code = translator.ToString(); - Assert.IsTrue(txt.Contains("Expression> TemplateTest.IMyTypeMapper.Projection")); - Assert.IsTrue(txt.Contains("AddressDTO TemplateTest.IMyTypeMapper.Map")); - Assert.IsTrue(txt.Contains("[MapsterToolGeneratedMapper]")); + Assert.IsTrue(code.Contains("internal partial class CustomerMapper")); // mapper class is internal + // force create internal method using only public types because mapper class is internal + Assert.IsTrue(code.Contains("internal AddressDTO Map")); } + + + } + + + public interface IMyTypeMapper + { + internal AddressDTO Map(Address p1); + public Expression> Projection { get; } + } + + internal interface IMyTypeMapperIntenal + { + AddressInternal MapInternal(Address p1); + Expression> ProjectionInternal { get; } + AddressDTO MapPublicClassInInternalInterface(Address p1); + internal AddressDTO MapPublicClassInInternalInterfaceWithMarkInternal(Address p1); } - internal interface IMyTypeMapper + public interface IMyTypeMapperForce { AddressDTO Map(Address p1); - Expression> Projection { get; } + } + + internal class AddressInternal + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } } public class Address @@ -188,4 +212,68 @@ public class CustomerDTO public List WorkAddresses { get; set; } public string AddressCity { get; set; } } + + static class GenerateMappersExtensions + { + public static void CreateFromInterface(this ExpressionTranslator translator, TypeDefinitions definitions, TypeAdapterConfig config) + { + if (definitions.Implements == null) + return; + + foreach (var interfaceType in definitions.Implements) + { + bool? _isForceInternal = definitions.IsInternal ? true : null; + + foreach (var method in interfaceType.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, + interfaceType, + method.Name, + _isForceInternal ?? !method.IsPublic + ); + } + + foreach (var prop in interfaceType.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, + interfaceType, + prop.Name, + _isForceInternal ?? (!prop.GetMethod?.IsPublic ?? false) + ); + } + } + } + } } \ No newline at end of file From 83e275057b2a298c2af5687f95f533bd71bb0426 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 21 Aug 2026 00:59:08 +0500 Subject: [PATCH 6/7] fix: refactoring attribute impl --- .../MapsterToolGeneratedMapperAttribute.cs | 16 +++++++--------- src/Mapster.Tool/MapperOptions.cs | 4 ++-- src/Mapster.Tool/Program.cs | 19 +++++++++---------- src/TemplateTest/CreateMapExpressionTest.cs | 6 +++--- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs index 18ea2e97..2f8c91de 100644 --- a/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs +++ b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs @@ -1,11 +1,11 @@ -using System.Text; +using System; +using System.Text; namespace ExpressionDebugger.Helpers.GeneratedAttributes { public class MapsterToolGeneratedMapperAttribute : GeneratedBase, IGeneratedAttribute { private readonly StringBuilder _Declaration; - private readonly bool _isRandomNameSpace; private readonly string _NameSpace; public string NameSpace => _NameSpace; @@ -16,15 +16,13 @@ public class MapsterToolGeneratedMapperAttribute : GeneratedBase, IGeneratedAttr public string FileName => "MapsterToolGeneratedMapperAttribute"; - public MapsterToolGeneratedMapperAttribute(bool isRandomNameSpace = false) + public MapsterToolGeneratedMapperAttribute(string extendedNameSpace) { - _isRandomNameSpace = isRandomNameSpace; - - if (_isRandomNameSpace) - _NameSpace = $"Mapster.Generated.Attributes.{RandomNamespaceGenerator.Generate(1,1)}"; - else - _NameSpace = "Mapster.Generated.Attributes"; + if (String.IsNullOrEmpty(extendedNameSpace)) + throw new ArgumentNullException("Extended namespace not specified or is null/empty string"); + _NameSpace = $"Mapster.Generated.Attributes.{extendedNameSpace}"; + _Declaration = new StringBuilder(); _Declaration.Append("using System;\r\n\r\n"); diff --git a/src/Mapster.Tool/MapperOptions.cs b/src/Mapster.Tool/MapperOptions.cs index f4725a9b..5ae244a1 100644 --- a/src/Mapster.Tool/MapperOptions.cs +++ b/src/Mapster.Tool/MapperOptions.cs @@ -28,8 +28,8 @@ public class MapperOptions [Option('N', "nullableDirective", Required = false, HelpText = "Set true to add \"#nullable enable\" to the top of generated mapper files")] public bool GenerateNullableDirective { get; set; } - [Option('H', "createHelpers", Required = false, HelpText = "Set true to create helper files")] - public bool CreateHelpers { get; set; } + [Option('H', "extNamespace", Required = false, HelpText = "Specify namespace to activate and generate additional features")] + public string? CreateHelpers { get; set; } [Usage(ApplicationAlias = "dotnet mapster mapper")] public static IEnumerable Examples => diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 79534284..aa46d3eb 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -93,7 +93,11 @@ private static void GenerateMappers(MapperOptions opt) config.SelfContainedCodeGeneration = true; config.Scan(assembly); - var generatedAtrr = new[] { new MapsterToolGeneratedMapperAttribute() }; + var generatedAtrr = new List(); + + if (!String.IsNullOrEmpty(opt.CreateHelpers)) + generatedAtrr.Add(new MapsterToolGeneratedMapperAttribute(opt.CreateHelpers)); + foreach (var type in assembly.GetLoadableTypes()) { @@ -113,12 +117,9 @@ private static void GenerateMappers(MapperOptions opt) TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)), IsInternal = attr.IsInternal, PrintFullTypeName = opt.PrintFullTypeName, - + GeneratedAttributes = new(generatedAtrr) }; - if (opt.CreateHelpers) - definitions.GeneratedAttributes = new(generatedAtrr); - bool? _isForceInternal = definitions.IsInternal ? true : null; var path = GetOutput(opt.Output, segments, definitions.TypeName); @@ -194,12 +195,10 @@ private static void GenerateMappers(MapperOptions opt) WriteFile(code, path); } - if (opt.CreateHelpers) + + foreach (var item in generatedAtrr) { - foreach (var item in generatedAtrr) - { - WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName)); - } + WriteFile(item.Declaration, GetOutput(opt.Output, null, item.FileName)); } } diff --git a/src/TemplateTest/CreateMapExpressionTest.cs b/src/TemplateTest/CreateMapExpressionTest.cs index 928adc32..b325b333 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -78,7 +78,7 @@ public void TestCreateProjectionExpression() [TestMethod] public void TestRegressionMapperGenerationTranslation() { - var S = new MapsterToolGeneratedMapperAttribute(true); + var S = new MapsterToolGeneratedMapperAttribute("Test"); var config = new TypeAdapterConfig(); config.SelfContainedCodeGeneration = true; @@ -89,7 +89,7 @@ public void TestRegressionMapperGenerationTranslation() Namespace = "Benchmark", TypeName = "CustomerMapper", IsInternal = false, - GeneratedAttributes = new(new[] {new MapsterToolGeneratedMapperAttribute()}) + GeneratedAttributes = new(new[] {new MapsterToolGeneratedMapperAttribute("Test") }) }; var translator = new ExpressionTranslator(definitions); @@ -129,7 +129,7 @@ public void CreateForceInternalMapper() Namespace = "Benchmark", TypeName = "CustomerMapper", IsInternal = true, // force create internal mapper - GeneratedAttributes = new(new[] { new MapsterToolGeneratedMapperAttribute() }) + GeneratedAttributes = new(new[] { new MapsterToolGeneratedMapperAttribute("Test") }) }; var translator = new ExpressionTranslator(definitions); From 7cf555302fb09d29656914b3908ef29c7a440409 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 21 Aug 2026 01:57:54 +0500 Subject: [PATCH 7/7] fix: skip explicit interface implimentation if using only public way: 1) property or method maks as public 2) property or method using only public clases 3) not using IsInternal atrribute --- src/ExpressionTranslator/ExpressionTranslator.cs | 8 ++++++++ src/TemplateTest/CreateMapExpressionTest.cs | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ExpressionTranslator/ExpressionTranslator.cs b/src/ExpressionTranslator/ExpressionTranslator.cs index e7ed9838..823bebcc 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.cs +++ b/src/ExpressionTranslator/ExpressionTranslator.cs @@ -11,6 +11,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Xml.Linq; namespace ExpressionDebugger { @@ -1273,6 +1274,13 @@ public Expression VisitLambdaForGenerateMappers(LambdaExpression node, LambdaTyp { VisitLambda(node, type, methodName, isInternal); + if (!isInternal) + isInternal = node.ReturnType.GetTypeInfo().IsNotPublic || + node.Parameters.Any(it => it.Type.GetTypeInfo().IsNotPublic); + + if(!isInternal) + return node; // skip create interface implimentation if public only + if (type == LambdaType.PrivateLambda || type == LambdaType.PublicLambda) { _inlineCount++; diff --git a/src/TemplateTest/CreateMapExpressionTest.cs b/src/TemplateTest/CreateMapExpressionTest.cs index b325b333..d71ab9d7 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -99,8 +99,6 @@ public void TestRegressionMapperGenerationTranslation() var code = translator.ToString(); Assert.IsTrue(code.Contains("public partial class CustomerMapper")); // mapper class is public - - Assert.IsTrue(code.Contains("Expression> TemplateTest.IMyTypeMapper.Projection")); Assert.IsTrue(code.Contains("AddressDTO TemplateTest.IMyTypeMapper.Map")); Assert.IsTrue(code.Contains("[MapsterToolGeneratedMapper]"));