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); } } }