From 3de9cdaf0748c2211199ec4fa58301ae3421bebb Mon Sep 17 00:00:00 2001 From: Casey Boyle Date: Mon, 17 Jun 2024 10:12:14 -0500 Subject: [PATCH] Added isolated assembly loading for issue #714 --- .github/workflows/dotnet-buildandtest.yml | 2 +- .gitignore | 5 + .../DeferredDependencyAssemblyLoadContext.cs | 87 +++++ .../IsolatedAssemblyLoadContext.cs | 76 ++++ src/Mapster.Tool/Program.cs | 347 +++++++++++++----- 5 files changed, 415 insertions(+), 102 deletions(-) create mode 100644 src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs create mode 100644 src/Mapster.Tool/IsolatedAssemblyLoadContext.cs diff --git a/.github/workflows/dotnet-buildandtest.yml b/.github/workflows/dotnet-buildandtest.yml index 7e30a04a..37dc390a 100644 --- a/.github/workflows/dotnet-buildandtest.yml +++ b/.github/workflows/dotnet-buildandtest.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.x.x - name: Show dotnet version run: | dotnet --list-sdks diff --git a/.gitignore b/.gitignore index 83a5664f..9c0a14cd 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,8 @@ project.lock.json /src/.vs /.vs src/.idea + +# VS Code settings +.vscode/launch.json +.vscode/settings.json +.vscode/tasks.json diff --git a/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs b/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs new file mode 100644 index 00000000..09132d22 --- /dev/null +++ b/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs @@ -0,0 +1,87 @@ +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Mapster.Tool +{ + // + // Summary: + // Used for loading an assembly and its dependencies in an isolated assembly load context but deferring the resolution of + // a subset of those assemblies to an already existing Assembly Load Context (likely the AssemblyLoadContext.Default + // context that is used by the runtime by default at startup) + public class DeferredDependencyAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver resolver; + private readonly ImmutableHashSet deferredDependencyAssemblyNames; + private readonly AssemblyLoadContext deferToContext; + + public DeferredDependencyAssemblyLoadContext( + string assemblyPath, + AssemblyLoadContext deferToContext, + params AssemblyName[] deferredDependencyAssemblyNames + ) + { + // set up a resolver for the dependencies of this non-deferred assembly + resolver = new AssemblyDependencyResolver(assemblyPath); + + // store all of the assembly simple names that should be deferred w/ + // the sharing assembly context loader (and not resolved exclusively in this loader) + this.deferredDependencyAssemblyNames = deferredDependencyAssemblyNames + .Select(an => an.Name!) + .Where(n => n != null) + .ToImmutableHashSet(); + + // store a reference to the assembly load context that assembly resolution will be deferred + // to when on the deferredDependencyAssemblyNames list + this.deferToContext = deferToContext; + + // load the non-deferred assembly in this context to start + Load(GetAssemblyName(assemblyPath)); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (assemblyName.Name == null) + { + return null; + } + + // if the assembly to be loaded is also set to be deferrred (based on constructor) + // then first attempt to load it from the sharing assembly load context + if (deferredDependencyAssemblyNames.Contains(assemblyName.Name)) + { + return deferToContext.LoadFromAssemblyName(assemblyName); + } + + // all other loaded assemblies should be considered dependencies of the + // non-deferred dependency loaded in the constructor and should be loaded + // from its path (the AssemblyDepedencyResolver resolves dependency paths) + string? assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath == null) + { + return null; + } + + return LoadFromAssemblyPath(assemblyPath); + } + + public static Assembly LoadAssemblyFrom( + string assemblyPath, + AssemblyLoadContext deferToContext, + params AssemblyName[] deferredDependencyAssemblyNames + ) + { + DeferredDependencyAssemblyLoadContext loadContext = + new DeferredDependencyAssemblyLoadContext( + assemblyPath, + deferToContext, + deferredDependencyAssemblyNames + ); + return loadContext.LoadFromAssemblyName( + new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath)) + ); + } + } +} diff --git a/src/Mapster.Tool/IsolatedAssemblyLoadContext.cs b/src/Mapster.Tool/IsolatedAssemblyLoadContext.cs new file mode 100644 index 00000000..45d5756f --- /dev/null +++ b/src/Mapster.Tool/IsolatedAssemblyLoadContext.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using CommandLine; +using ExpressionDebugger; +using Mapster.Models; +using Mapster.Utils; +using System; +using System.Reflection; +using System.Runtime.Loader; + +namespace Mapster.Tool +{ + public class IsolatedAssemblyContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver resolver; + + public IsolatedAssemblyContext(string assemblyPath) + { + resolver = new AssemblyDependencyResolver(assemblyPath); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + string assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string libraryPath = resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } + + public static Assembly LoadAssemblyFrom(string relativePath) + { + // Navigate up to the solution root + string root = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName( + Path.GetDirectoryName( + Path.GetDirectoryName( + Path.GetDirectoryName( + Path.GetDirectoryName(typeof(Program).Assembly.Location) + ) + ) + ) + ) + ) + ); + + string assemblyLocation = Path.GetFullPath( + Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)) + ); + Console.WriteLine($"Loading commands from: {assemblyLocation}"); + IsolatedAssemblyContext loadContext = new IsolatedAssemblyContext(assemblyLocation); + return loadContext.LoadFromAssemblyName( + new AssemblyName(Path.GetFileNameWithoutExtension(assemblyLocation)) + ); + } + } +} diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 4066ef52..633f23ed 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.Loader; using System.Text; using CommandLine; using ExpressionDebugger; @@ -16,7 +17,8 @@ class Program { static void Main(string[] args) { - Parser.Default.ParseArguments(args) + Parser.Default + .ParseArguments(args) .WithParsed(GenerateMappers) .WithParsed(GenerateModels) .WithParsed(GenerateExtensions); @@ -39,9 +41,13 @@ static void Main(string[] args) 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"); + 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) @@ -60,7 +66,26 @@ private static void WriteFile(string code, string path) private static void GenerateMappers(MapperOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + // 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, + deferredDependencyAssemblyNames: typeof(MapperAttribute).Assembly.GetName() + ); var config = TypeAdapterConfig.GlobalSettings; config.SelfContainedCodeGeneration = true; config.Scan(assembly); @@ -78,7 +103,7 @@ private static void GenerateMappers(MapperOptions opt) var segments = GetSegments(type.Namespace, opt.BaseNamespace); var definitions = new TypeDefinitions { - Implements = new[] {type}, + Implements = new[] { type }, Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace), TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)), IsInternal = attr.IsInternal, @@ -88,7 +113,9 @@ private static void GenerateMappers(MapperOptions opt) var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { - Console.WriteLine($"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists."); + Console.WriteLine( + $"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists." + ); continue; } @@ -110,8 +137,11 @@ 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(expr, ExpressionTranslator.LambdaType.PublicLambda, - prop.Name); + translator.VisitLambda( + expr, + ExpressionTranslator.LambdaType.PublicLambda, + prop.Name + ); } } @@ -127,16 +157,21 @@ private static void GenerateMappers(MapperOptions opt) 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.VisitLambda(expr, ExpressionTranslator.LambdaType.PublicMethod, - method.Name); + var expr = config.CreateMapExpression( + tuple, + methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget + ); + translator.VisitLambda( + expr, + ExpressionTranslator.LambdaType.PublicMethod, + method.Name + ); } } - var code = opt.GenerateNullableDirective ? - $"#nullable enable{Environment.NewLine}{translator}" : - translator.ToString(); + var code = opt.GenerateNullableDirective + ? $"#nullable enable{Environment.NewLine}{translator}" + : translator.ToString(); WriteFile(code, path); } } @@ -150,7 +185,11 @@ private static string GetImplName(string name) private static void GenerateModels(ModelOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom( + assemblyPath: Path.GetFullPath(opt.Assembly), + deferToContext: AssemblyLoadContext.Default, + deferredDependencyAssemblyNames: typeof(MapperAttribute).Assembly.GetName() + ); var codeGenConfig = new CodeGenerationConfig(); codeGenConfig.Scan(assembly); @@ -165,7 +204,11 @@ private static void GenerateModels(ModelOptions opt) foreach (var type in types) { var builders = type.GetAdaptAttributeBuilders(codeGenConfig) - .Where(it => !string.IsNullOrEmpty(it.Attribute.Name) && it.Attribute.Name != "[name]") + .Where( + it => + !string.IsNullOrEmpty(it.Attribute.Name) + && it.Attribute.Name != "[name]" + ) .ToList(); if (builders.Count == 0) continue; @@ -182,10 +225,13 @@ private static void GenerateModels(ModelOptions opt) { var nilCtxAttr = type.GetCustomAttributesData() .FirstOrDefault(it => it.AttributeType.Name == "NullableContextAttribute"); - return nilCtxAttr?.ConstructorArguments.Count == 1 && nilCtxAttr.ConstructorArguments[0].Value is byte b - ? (byte?) b + 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); @@ -202,7 +248,9 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { - Console.WriteLine($"Skipped: {type.FullName}. Model {definitions.TypeName} already exists."); + Console.WriteLine( + $"Skipped: {type.FullName}. Model {definitions.TypeName} already exists." + ); return; } @@ -210,33 +258,43 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild 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)); + 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()); + 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()); + 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); + properties = properties.Where( + it => getPropType(it).Namespace?.StartsWith(ns) != true + ); } } @@ -248,47 +306,74 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild 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() + 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 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(); + 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; + 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) + 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() + 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) + if ( + propType.IsCollection() + && propType.IsCollectionCompatible() + && propType.IsGenericType + && propType.GetGenericArguments().Length == 1 + ) { var elementType = propType.GetGenericArguments()[0]; var newType = GetPropertyType(member, elementType, attrType, ns, builder); @@ -305,14 +390,16 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT return alterType; var propTypeAttrs = propType.SafeGetCustomAttributes(); - navAttr = propTypeAttrs.OfType() + 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() + ? (BaseAdaptAttribute?)builder.Attribute + : propTypeAttrs + .OfType() .FirstOrDefault(it => it.GetType() == attrType); if (adaptAttr == null) return propType; @@ -330,7 +417,7 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT private static Type? GetFromType(Type type, BaseAdaptAttribute attr, HashSet types) { - if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute)) + if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute)) return null; var fromType = attr.Type; @@ -345,7 +432,7 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT private static Type? GetToType(Type type, BaseAdaptAttribute attr, HashSet types) { - if (!(attr is AdaptToAttribute)) + if (!(attr is AdaptToAttribute)) return null; var toType = attr.Type; @@ -358,19 +445,25 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT return toType; } - private static void ApplySettings(TypeAdapterSetter setter, BaseAdaptAttribute attr, Dictionary settings) + 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, - }); + setter.Settings.Resolvers.Add( + new InvokerModel + { + DestinationMemberName = setting.TargetPropertyName ?? name, + SourceMemberName = name, + Invoker = setting.MapFunc, + } + ); } else if (setting.TargetPropertyName != null) { @@ -381,14 +474,18 @@ private static void ApplySettings(TypeAdapterSetter setter, BaseAdaptAttribute a private static void GenerateExtensions(ExtensionOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom( + assemblyPath: Path.GetFullPath(opt.Assembly), + deferToContext: AssemblyLoadContext.Default, + deferredDependencyAssemblyNames: typeof(MapperAttribute).Assembly.GetName() + ); var config = TypeAdapterConfig.GlobalSettings; config.SelfContainedCodeGeneration = true; config.Scan(assembly); var codeGenConfig = new CodeGenerationConfig(); codeGenConfig.Scan(assembly); - var assemblies = new HashSet {assembly}; + var assemblies = new HashSet { assembly }; foreach (var builder in codeGenConfig.AdaptAttributeBuilders) { foreach (var setting in builder.TypeSettings) @@ -399,7 +496,8 @@ private static void GenerateExtensions(ExtensionOptions opt) 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); + foreach (var (key, _) in config.RuleMap) + types.Add(key.Source); var configDict = new Dictionary(); foreach (var builder in codeGenConfig.AdaptAttributeBuilders) { @@ -423,8 +521,9 @@ private static void GenerateExtensions(ExtensionOptions opt) { var mapperAttr = type.GetGenerateMapperAttributes(codeGenConfig).FirstOrDefault(); var ruleMaps = config.RuleMap - .Where(it => it.Key.Source == type && - it.Value.Settings.GenerateMapper is MapType) + .Where( + it => it.Key.Source == type && it.Value.Settings.GenerateMapper is MapType + ) .ToList(); if (mapperAttr == null && ruleMaps.Count == 0) continue; @@ -452,7 +551,9 @@ private static void GenerateExtensions(ExtensionOptions opt) 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."); + Console.WriteLine( + $"Skipped: {type.FullName}. Extension class {definitions.TypeName} already exists." + ); continue; } @@ -466,67 +567,109 @@ private static void GenerateExtensions(ExtensionOptions opt) 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 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); + 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 mapType = (MapType)rule.Settings.GenerateMapper!; + GenerateExtensionMethods( + mapType, + config, + tuple, + translator, + type, + mapperAttr.IsHelperClass + ); } - var code = opt.GenerateNullableDirective ? - $"#nullable enable{Environment.NewLine}{translator}" : - translator.ToString(); + 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) + 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, ""); + 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); + 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"); + 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); + translator.VisitLambda( + proj, + ExpressionTranslator.LambdaType.PublicLambda, + "ProjectTo" + name + ); } } - private static string GetCodeFriendlyTypeName(Type type) => GetCodeFriendlyTypeName(new StringBuilder(), type).ToString(); + private static string GetCodeFriendlyTypeName(Type type) => + GetCodeFriendlyTypeName(new StringBuilder(), type).ToString(); private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type) { @@ -544,7 +687,8 @@ private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type var name = type.Name; var i = name.IndexOf('`'); - if (i>0) name = name.Remove(i); + if (i > 0) + name = name.Remove(i); name = name switch { "SByte" => "Sbyte", @@ -559,8 +703,9 @@ private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type _ => name, }; - if (!string.IsNullOrEmpty(name)) sb.Append(name); + if (!string.IsNullOrEmpty(name)) + sb.Append(name); return sb; } } -} \ No newline at end of file +}