diff --git a/src/ExpressionTranslator/ExpressionTranslator.cs b/src/ExpressionTranslator/ExpressionTranslator.cs index 6c219f86..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 { @@ -1268,6 +1269,88 @@ 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 (!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++; + 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; @@ -1865,9 +1948,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) @@ -1891,6 +1981,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..2f8c91de --- /dev/null +++ b/src/ExpressionTranslator/Helpers/GeneratedAttributes/MapsterToolGeneratedMapperAttribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Text; + +namespace ExpressionDebugger.Helpers.GeneratedAttributes +{ + public class MapsterToolGeneratedMapperAttribute : GeneratedBase, IGeneratedAttribute + { + private readonly StringBuilder _Declaration; + private readonly string _NameSpace; + + public string NameSpace => _NameSpace; + + public string Declaration => _Declaration.ToString(); + + public string Implimentation => "[MapsterToolGeneratedMapper]"; + + public string FileName => "MapsterToolGeneratedMapperAttribute"; + + public MapsterToolGeneratedMapperAttribute(string extendedNameSpace) + { + 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"); + _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/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/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/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/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/MapperOptions.cs b/src/Mapster.Tool/MapperOptions.cs index 9cd8a1aa..5ae244a1 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', "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 => new List diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 1347cb13..aa46d3eb 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -1,4 +1,10 @@ -using System; +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; @@ -6,10 +12,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 +93,12 @@ private static void GenerateMappers(MapperOptions opt) config.SelfContainedCodeGeneration = true; config.Scan(assembly); + var generatedAtrr = new List(); + + if (!String.IsNullOrEmpty(opt.CreateHelpers)) + generatedAtrr.Add(new MapsterToolGeneratedMapperAttribute(opt.CreateHelpers)); + + foreach (var type in assembly.GetLoadableTypes()) { if (!type.IsInterface) @@ -109,8 +117,11 @@ private static void GenerateMappers(MapperOptions opt) 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)) { @@ -124,7 +135,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; @@ -138,17 +151,21 @@ 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, - prop.Name + @interface, + 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; @@ -162,10 +179,12 @@ private static void GenerateMappers(MapperOptions opt) tuple, methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget ); - translator.VisitLambda( + translator.VisitLambdaForGenerateMappers( expr, ExpressionTranslator.LambdaType.PublicMethod, - method.Name + @interface, + method.Name, + _isForceInternal ?? !method.IsPublic ); } } @@ -175,6 +194,12 @@ 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..d71ab9d7 100644 --- a/src/TemplateTest/CreateMapExpressionTest.cs +++ b/src/TemplateTest/CreateMapExpressionTest.cs @@ -1,7 +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 { @@ -64,6 +71,107 @@ public void TestCreateProjectionExpression() Assert.IsNotNull(code); } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/399 + /// + [TestMethod] + public void TestRegressionMapperGenerationTranslation() + { + var S = new MapsterToolGeneratedMapperAttribute("Test"); + + var config = new TypeAdapterConfig(); + config.SelfContainedCodeGeneration = true; + + var definitions = new TypeDefinitions + { + Implements = new[] { typeof(IMyTypeMapper), typeof(IMyTypeMapperIntenal) }, + Namespace = "Benchmark", + TypeName = "CustomerMapper", + IsInternal = false, + GeneratedAttributes = new(new[] {new MapsterToolGeneratedMapperAttribute("Test") }) + }; + + var translator = new ExpressionTranslator(definitions); + + translator.CreateFromInterface(definitions, config); + + var code = translator.ToString(); + + Assert.IsTrue(code.Contains("public partial class CustomerMapper")); // mapper class is public + 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; + + var definitions = new TypeDefinitions + { + Implements = new[] { typeof(IMyTypeMapperForce)}, + Namespace = "Benchmark", + TypeName = "CustomerMapper", + IsInternal = true, // force create internal mapper + GeneratedAttributes = new(new[] { new MapsterToolGeneratedMapperAttribute("Test") }) + }; + + var translator = new ExpressionTranslator(definitions); + + translator.CreateFromInterface(definitions, config); + + var code = translator.ToString(); + + 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); + } + + public interface IMyTypeMapperForce + { + AddressDTO Map(Address p1); + } + + 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 @@ -102,4 +210,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