From 530066fbb925fe4bb8529babd77bd5b58a191f54 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 18 Mar 2024 00:43:51 -0600 Subject: [PATCH] feat: Inline user implemented queryable mapping expressions (#1169) Tries to inline user-implemented methods with an expression body into queryable expression mappings. This leads to better query mappings by EF Core and should result in less data loaded. If the inlining fails for some reason (e.g. not a valid c# expression tree), RMG068 is reported. This works with a syntax rewriter (InlineExpressionRewriter) which * extends type names to their fully qualified names (since the using statements are not present in the generated code) * expands extension method invocations (since the using statements are not present in the generated code) * extracts invocations of other user-defined mappings, to be inlined by the mapping itself * tries to assume whether the given code can be successfully inlined in a c# expression tree by checking the presence of given syntax declarations Additionally a new mapping is added (UserImplementedInlinedExpressionMapping) which represents an inlined version of a user implemented mapping. When building the mapping, it rewrites the source parameter of the user-method to the actual source of the mapping. Additionally mapping invocations extracted by the InlineExpressionRewriter are inlined. Fixes #953. --- docs/docs/configuration/flattening.md | 2 +- .../configuration/queryable-projections.mdx | 50 ++++- .../user-implemented-methods.mdx | 2 + docs/docs/contributing/architecture.md | 7 + src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 1 + .../Descriptors/DescriptorBuilder.cs | 10 +- .../ExternalMappingsExtractor.cs | 7 +- .../InlineExpressionMappingBuilderContext.cs | 106 ++++++--- .../Descriptors/InlineExpressionRewriter.cs | 203 ++++++++++++++++++ .../InlinedExpressionMappingCollection.cs | 82 +++++++ .../InlineExpressionMappingBuilder.cs | 45 ++++ .../MappingBuilders/MappingBuilder.cs | 2 +- .../Descriptors/MappingCollection.cs | 89 ++++---- .../Descriptors/MappingCollectionAddResult.cs | 1 - .../Mappings/UserMappings/IUserMapping.cs | 6 + .../UserDefinedExistingTargetMethodMapping.cs | 2 + .../UserDefinedNewInstanceMethodMapping.cs | 2 + ...inedNewInstanceRuntimeTargetTypeMapping.cs | 2 + ...rImplementedExistingTargetMethodMapping.cs | 5 +- ...UserImplementedInlinedExpressionMapping.cs | 68 ++++++ .../UserImplementedMethodMapping.cs | 4 +- .../SimpleMappingBuilderContext.cs | 9 + .../Descriptors/UserMethodMappingExtractor.cs | 32 ++- .../Diagnostics/DiagnosticDescriptors.cs | 10 + .../Dto/LongValueDto.cs | 7 + .../Dto/TestObjectDtoProjection.cs | 4 + .../Mapper/ProjectionMapper.cs | 17 +- .../Models/IntegerValue.cs | 9 + .../Models/LongValue.cs | 9 + .../Models/TestObjectProjection.cs | 4 + ...jectionShouldTranslateToQuery.verified.sql | 6 +- ...ShouldTranslateToQuery_NET8_0.verified.sql | 6 +- ...erTest.SnapshotGeneratedSource.verified.cs | 23 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 21 +- .../Helpers/InlineExpressionRewriterTest.cs | 80 +++++++ .../Mapping/QueryableProjectionTest.cs | 22 +- .../QueryableProjectionUseNamedMappingTest.cs | 32 +++ .../QueryableProjectionUserImplementedTest.cs | 122 +++++++++++ test/Riok.Mapperly.Tests/TestHelper.cs | 4 + test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 2 +- ...lFlatteningInsideList#Mapper.g.verified.cs | 4 +- ...cordManualListMapping#Mapper.g.verified.cs | 2 +- ...est.ReferenceLoopCtor#Mapper.g.verified.cs | 5 +- ...renceLoopInitProperty#Mapper.g.verified.cs | 4 + ...gOnSelectedProperties#Mapper.g.verified.cs | 6 +- ...abledAutoUserMappings#Mapper.g.verified.cs | 6 +- ...gOnSelectedProperties#Mapper.g.verified.cs | 4 +- ...abledAutoUserMappings#Mapper.g.verified.cs | 4 +- ...WithNamedUsedMappings#Mapper.g.verified.cs | 35 +++ ...pingsAndAmbiguousName#Mapper.g.verified.cs | 35 +++ ...dUsedMappingsAndAmbiguousName.verified.txt | 24 +++ ...lassInlinedExpression#Mapper.g.verified.cs | 17 ++ ...lassNonInlinedMethod#Mapper.g.verified.cs} | 0 ....ClassToClassNonInlinedMethod.verified.txt | 14 ++ ...erImplementedOrdering#Mapper.g.verified.cs | 17 ++ ...MappingAndMethodGroup#Mapper.g.verified.cs | 0 ...ingAndParameterHiding#Mapper.g.verified.cs | 28 +++ ...WithTwoNestedMappings#Mapper.g.verified.cs | 28 +++ ...hesizedLambdaOrdering#Mapper.g.verified.cs | 17 ++ ...ssWithUserImplemented#Mapper.g.verified.cs | 0 60 files changed, 1209 insertions(+), 156 deletions(-) create mode 100644 src/Riok.Mapperly/Descriptors/InlineExpressionRewriter.cs create mode 100644 src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Dto/LongValueDto.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Models/IntegerValue.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Models/LongValue.cs create mode 100644 test/Riok.Mapperly.Tests/Helpers/InlineExpressionRewriterTest.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappings#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedExpression#Mapper.g.verified.cs rename test/Riok.Mapperly.Tests/_snapshots/{QueryableProjectionTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs => QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod#Mapper.g.verified.cs} (100%) create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrdering#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndMethodGroup#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndParameterHiding#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithTwoNestedMappings#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedParenthesizedLambdaOrdering#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs diff --git a/docs/docs/configuration/flattening.md b/docs/docs/configuration/flattening.md index 0bc98b4363..19838514e8 100644 --- a/docs/docs/configuration/flattening.md +++ b/docs/docs/configuration/flattening.md @@ -11,7 +11,7 @@ If Mapperly can't resolve the target or source property correctly, it is possibl by either using the source and target property path names as arrays or using a dot separated property access path string ```csharp -[MapProperty(new[] { nameof(Car.Make), nameof(Car.Make.Id) }, new[] { nameof(CarDto.MakeId) })] +[MapProperty([nameof(Car.Make), nameof(Car.Make.Id)], [nameof(CarDto.MakeId)])] // Or alternatively [MapProperty("Make.Id", "MakeId")] // Or diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 09c6f68a2c..b954501b81 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -53,9 +53,7 @@ such mappings have several limitations: - Enum mappings do not support the `ByName` strategy - Reference handling is not supported - Nullable reference types are disabled -- User implemented mapping methods need to follow expression tree [limitations](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations). - -::: + ::: ## Property configurations @@ -76,3 +74,49 @@ public static partial class CarMapper private static partial CarDto Map(Car car); } ``` + +## User-implemented mapping methods + +Mapperly tries to inline user-implemented mapping methods. +For this to work, user-implemented mapping methods need to satisfy certain limitations: + +- Only expression-bodied methods can be inlined. +- The body needs to follow the [expression tree limitations](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations). +- Nested MethodGroups cannot be inlined. + +```csharp +[Mapper] +public static partial class CarMapper +{ + public static partial IQueryable ProjectToDto(this IQueryable q); + + // highlight-start + private static string MapCarBrandName(CarBrand brand) + => band.Name; + // highlight-end +} +``` + +If Mapperly is unable to inline a user-implemented method, `RMG068` is reported. +Non-inlined method invocations can lead to more data being loaded than necessary. + +Ordering and aggregating collections can also be implemented with user-implemented mapping methods: + +```csharp +[Mapper] +public static partial class CarMapper +{ + public static partial IQueryable ProjectToDto(this IQueryable q); + + private static partial CarModelDto MapCarModel(CarModel model); + + // highlight-start + private static ICollection MapOrderedCarBrandName(ICollection models) + // note: do not use the method group '.Select(MapCarModel)', as that cannot be inlined + => models.OrderBy(x => x.Name).Select(x => MapCarModel(x)).ToList(); + // highlight-end + } +``` + +It is important that the types in the user-implemented mapping method match the types of the objects to be mapped exactly. +Otherwise, Mapperly cannot resolve the user-implemented mapping methods. diff --git a/docs/docs/configuration/user-implemented-methods.mdx b/docs/docs/configuration/user-implemented-methods.mdx index ec4b5f9889..96ae555823 100644 --- a/docs/docs/configuration/user-implemented-methods.mdx +++ b/docs/docs/configuration/user-implemented-methods.mdx @@ -21,6 +21,8 @@ public partial class CarMapper ``` Whenever Mapperly needs a mapping from `TimeSpan` to `int` inside the `CarMapper` implementation, it will use the provided implementation. +The types of the user-implemented mapping method need to match the types to map exactly, including nullability. + If there are multiple user-implemented mapping methods suiting the given type-pair, by default, the first one is used. This can be customized by using [automatic user-implemented mapping method discovery](#automatic-user-implemented-mapping-method-discovery) and [default user-implemented mapping method](#default-user-implemented-mapping-method). diff --git a/docs/docs/contributing/architecture.md b/docs/docs/contributing/architecture.md index 44f23bcf25..92285ac09d 100644 --- a/docs/docs/contributing/architecture.md +++ b/docs/docs/contributing/architecture.md @@ -60,3 +60,10 @@ while still supporting older compiler versions. See `build/package.sh` for details. To introduce support for a new roslyn version see [common tasks](./common-tasks.md#add-support-for-a-new-roslyn-version). + +## C# language features + +The Mapperly source generator targets `.netstandard2.0` but uses the latest C# language version +and polyfills generated by [`Meziantou.Polyfill`](https://github.com/meziantou/Meziantou.Polyfill). +Some newer C# language features require new runtime features and therefore cannot be used in Mapperly. +A good C# language features requirements overview is available [here](https://sergeyteplyakov.github.io/Blog/c%23/2024/03/06/CSharp_Language_Features_vs_Target_Frameworks.html). diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index d9b0e0820d..44006c8c6e 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -152,3 +152,4 @@ RMG064 | Mapper | Error | Cannot configure an object mapping on a non-obje RMG065 | Mapper | Warning | Cannot configure an object mapping on a queryable projection mapping, apply the configurations to an object mapping method instead RMG066 | Mapper | Warning | No members are mapped in an object mapping RMG067 | Mapper | Error | Invalid usage of the MapPropertyAttribute +RMG068 | Mapper | Info | Cannot inline user implemented queryable expression mapping diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 429fe9ccb3..f8e195ceb4 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -23,6 +23,8 @@ public class DescriptorBuilder private readonly WellKnownTypes _types; private readonly MappingCollection _mappings = new(); + private readonly InlinedExpressionMappingCollection _inlineMappings = new(); + private readonly MethodNameBuilder _methodNameBuilder = new(); private readonly MappingBodyBuilder _mappingBodyBuilder; private readonly SimpleMappingBuilderContext _builderContext; @@ -60,6 +62,7 @@ MapperConfiguration defaultMapperConfiguration _diagnostics, new MappingBuilder(_mappings, mapperDeclaration), new ExistingTargetMappingBuilder(_mappings), + _inlineMappings, mapperDeclaration.Syntax.GetLocation() ); } @@ -212,8 +215,9 @@ private void AddAccessorsToDescriptor() private void AddUserMapping(IUserMapping mapping, bool ignoreDuplicates, bool named) { - var result = _mappings.AddUserMapping(mapping, ignoreDuplicates, named ? mapping.Method.Name : null); - if (result == MappingCollectionAddResult.NotAddedDuplicatedDefault) + var name = named ? mapping.Method.Name : null; + var result = _mappings.AddUserMapping(mapping, name); + if (!ignoreDuplicates && mapping.Default == true && result == MappingCollectionAddResult.NotAddedDuplicated) { _diagnostics.ReportDiagnostic( DiagnosticDescriptors.MultipleDefaultUserMappings, @@ -222,6 +226,8 @@ private void AddUserMapping(IUserMapping mapping, bool ignoreDuplicates, bool na mapping.TargetType.ToDisplayString() ); } + + _inlineMappings.AddUserMapping(mapping, name); } private void AddUserMappingDiagnostics() diff --git a/src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs b/src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs index 079fd007db..944816ff53 100644 --- a/src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs @@ -18,8 +18,9 @@ public static IEnumerable ExtractExternalMappings(SimpleMappingBui UserMethodMappingExtractor.ExtractUserImplementedMappings( ctx, x.MapperType, - x.MapperType.FullyQualifiedIdentifierName(), - true + receiver: x.MapperType.FullyQualifiedIdentifierName(), + isStatic: true, + isExternal: true ) ); @@ -44,7 +45,7 @@ private static IEnumerable ValidateAndExtractExternalInstanceMappi return Enumerable.Empty(); if (nullableAnnotation != NullableAnnotation.Annotated) - return UserMethodMappingExtractor.ExtractUserImplementedMappings(ctx, type, name, false); + return UserMethodMappingExtractor.ExtractUserImplementedMappings(ctx, type, name, isStatic: false, isExternal: true); ctx.ReportDiagnostic(DiagnosticDescriptors.ExternalMapperMemberCannotBeNullable, symbol, symbol.ToDisplayString()); return Enumerable.Empty(); diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 4daf624839..0257c7faa7 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -1,8 +1,11 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors.MappingBuilders; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Descriptors.Mappings.UserMappings; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Descriptors; @@ -12,15 +15,8 @@ namespace Riok.Mapperly.Descriptors; /// public class InlineExpressionMappingBuilderContext : MappingBuilderContext { - private readonly MappingCollection _inlineExpressionMappings; - private readonly MappingBuilderContext _parentContext; - public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, TypeMappingKey mappingKey) - : base(ctx, (ctx.FindMapping(mappingKey) as IUserMapping)?.Method, null, mappingKey, false) - { - _parentContext = ctx; - _inlineExpressionMappings = new MappingCollection(); - } + : base(ctx, (ctx.FindMapping(mappingKey) as IUserMapping)?.Method, null, mappingKey, false) { } private InlineExpressionMappingBuilderContext( InlineExpressionMappingBuilderContext ctx, @@ -29,11 +25,7 @@ private InlineExpressionMappingBuilderContext( TypeMappingKey mappingKey, bool ignoreDerivedTypes ) - : base(ctx, userSymbol, diagnosticLocation, mappingKey, ignoreDerivedTypes) - { - _parentContext = ctx; - _inlineExpressionMappings = ctx._inlineExpressionMappings; - } + : base(ctx, userSymbol, diagnosticLocation, mappingKey, ignoreDerivedTypes) { } public override bool IsExpression => true; @@ -47,40 +39,68 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi /// /// - /// Only returns s. + /// Only inline expression mappings and user implemented mappings are considered. + /// User implemented mappings are tried to be inlined. /// public override INewInstanceMapping? FindNamedMapping(string mappingName) { - // Only user implemented mappings are taken into account. - // This works as long as the user implemented methods - // follow the expression tree limitations: - // https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations - if (base.FindNamedMapping(mappingName) is INewInstanceUserMapping mapping) + var mapping = InlinedMappings.FindNamed(mappingName, out var ambiguousName, out var isInlined); + if (mapping == null) + { + // resolve named but not yet discovered mappings + mapping = base.FindNamedMapping(mappingName); + isInlined = false; + + if (mapping == null) + return null; + } + else if (ambiguousName) + { + ReportDiagnostic(DiagnosticDescriptors.ReferencedMappingAmbiguous, mappingName); + } + + if (isInlined) return mapping; - return null; + mapping = TryInlineMapping(mapping); + InlinedMappings.SetInlinedMapping(mappingName, mapping); + return mapping; } /// /// Tries to find an existing mapping for the provided types + config. /// The nullable annotation of reference types is ignored and always set to non-nullable. /// Only inline expression mappings and user implemented mappings are considered. + /// User implemented mappings are tried to be inlined. /// /// The mapping key. /// The if a mapping was found or null if none was found. public override INewInstanceMapping? FindMapping(TypeMappingKey mappingKey) { - if (_inlineExpressionMappings.FindNewInstanceMapping(mappingKey) is { } mapping) + var mapping = InlinedMappings.Find(mappingKey, out var isInlined); + if (mapping == null) + return null; + + if (isInlined) return mapping; - // User implemented mappings are also taken into account. - // This works as long as the user implemented methods - // follow the expression tree limitations: - // https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations - if (_parentContext.FindMapping(mappingKey) is UserImplementedMethodMapping userMapping) - return userMapping; + mapping = TryInlineMapping(mapping); + InlinedMappings.SetInlinedMapping(mappingKey, mapping); + return mapping; + } + + public INewInstanceMapping? FindNewInstanceMapping(IMethodSymbol method) + { + INewInstanceMapping? mapping = InlinedMappings.FindNewInstanceUserMapping(method, out var isInlined); + if (mapping == null) + return null; + + if (isInlined) + return mapping; - return null; + mapping = TryInlineMapping(mapping); + InlinedMappings.SetInlinedMapping(new TypeMappingKey(mapping), mapping); + return mapping; } /// @@ -122,13 +142,16 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi // unset mark as reusable as an inline expression mapping // should never be reused by the default mapping builder context, // only by other inline mapping builder contexts. - var reusable = (options & MappingBuildingOptions.MarkAsReusable) != MappingBuildingOptions.MarkAsReusable; + var reusable = options.HasFlag(MappingBuildingOptions.MarkAsReusable); options &= ~MappingBuildingOptions.MarkAsReusable; var mapping = base.BuildMapping(userSymbol, mappingKey, options, diagnosticLocation); - if (reusable && mapping != null) + if (mapping == null) + return null; + + if (reusable) { - _inlineExpressionMappings.AddMapping(mapping, mappingKey.Configuration); + InlinedMappings.AddMapping(mapping, mappingKey.Configuration); } return mapping; @@ -174,4 +197,25 @@ protected override MappingBuilderContext ContextForMapping( options.HasFlag(MappingBuildingOptions.IgnoreDerivedTypes) ); } + + private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) + { + return mapping switch + { + // inline existing mapping + UserImplementedMethodMapping implementedMapping + => InlineExpressionMappingBuilder.TryBuildMapping(this, implementedMapping) ?? implementedMapping, + + // build an inlined version + IUserMapping userMapping + => BuildMapping( + userMapping.Method, + new TypeMappingKey(userMapping), + MappingBuildingOptions.Default, + userMapping.Method.GetSyntaxLocation() + ) ?? mapping, + + _ => mapping, + }; + } } diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionRewriter.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionRewriter.cs new file mode 100644 index 0000000000..fec9b52112 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionRewriter.cs @@ -0,0 +1,203 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors; + +/// +/// Prepares an expression body to be used in an inlined expression mapping. +/// * annotates which target another mapping with a of kind . +/// * expands type names to their fully qualified names +/// * expands extension method invocations +/// * expands static method invocation receiver type names +/// Note: does not support method groups. +/// +/// The semantic model +/// To resolve mappings for a given method invocation +public class InlineExpressionRewriter(SemanticModel semanticModel, Func mappingResolver) + : CSharpSyntaxRewriter +{ + public const string SyntaxAnnotationKindMapperInvocation = "mapperInvocation"; + + private readonly Dictionary _mappingInvocations = new(); + + /// + /// A dictionary which maps annotations to the matching mappings. + /// Each annotation is attached to a . + /// + public IReadOnlyDictionary MappingInvocations => _mappingInvocations; + + /// + /// Whether the processed expression can successfully be inlined. + /// The expression needs to follow all the expression tree limitations + /// listed here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/expression-tree-restrictions#expression-tree-restrictions. + /// + public bool CanBeInlined { get; private set; } = true; + + public override SyntaxNode VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) + { + var fullyQualified = FullyQualifiedParentType(node.Type); + node = (ObjectCreationExpressionSyntax)base.VisitObjectCreationExpression(node)!; + if (fullyQualified != null) + { + node = node.WithType(fullyQualified); + } + + return node; + } + + public override SyntaxNode VisitTypeArgumentList(TypeArgumentListSyntax node) + { + var args = node.Arguments.Select(a => FullyQualifiedParentType(a) ?? a); + return node.WithArguments(SeparatedList(args)); + } + + public override SyntaxNode VisitArrayType(ArrayTypeSyntax node) + { + var fullyQualifiedElementType = FullyQualifiedType(node.ElementType); + if (fullyQualifiedElementType != null) + { + node = node.WithElementType(fullyQualifiedElementType); + } + + return node; + } + + public override SyntaxNode VisitTypeOfExpression(TypeOfExpressionSyntax node) + { + var fullyQualifiedElementType = FullyQualifiedParentType(node.Type); + if (fullyQualifiedElementType != null) + { + node = node.WithType(fullyQualifiedElementType); + } + + return node; + } + + public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node) + { + // expand static method invocation type names + // and extension method invocations + if (semanticModel.GetSymbolInfo(node).Symbol is not IMethodSymbol methodSymbol) + return base.VisitInvocationExpression(node); + + if (node.ArgumentList.Arguments.Count == 1 && mappingResolver.Invoke(methodSymbol) is { } mapping) + { + var annotation = new SyntaxAnnotation(SyntaxAnnotationKindMapperInvocation); + _mappingInvocations.Add(annotation, mapping); + node = node.WithAdditionalAnnotations(annotation); + } + + return methodSymbol switch + { + { IsExtensionMethod: true } => VisitExtensionMethodInvocation(node, methodSymbol), + { IsStatic: true } => VisitStaticMethodInvocation(node, methodSymbol), + _ => base.VisitInvocationExpression(node), + }; + } + + public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) + { + // CS8072 + CanBeInlined = false; + return base.VisitConditionalAccessExpression(node); + } + + public override SyntaxNode? VisitWithExpression(WithExpressionSyntax node) + { + // CS8849 + CanBeInlined = false; + return base.VisitWithExpression(node); + } + + public override SyntaxNode? VisitBaseExpression(BaseExpressionSyntax node) + { + // CS0831 + CanBeInlined = false; + return base.VisitBaseExpression(node); + } + +#if ROSLYN4_7_OR_GREATER + public override SyntaxNode VisitCollectionExpression(CollectionExpressionSyntax node) + { + // CS9175 + CanBeInlined = false; + return node; + } +#endif + + public override SyntaxNode VisitRangeExpression(RangeExpressionSyntax node) + { + // CS8792 + CanBeInlined = false; + return node; + } + + public override SyntaxNode VisitTupleExpression(TupleExpressionSyntax node) + { + // CS8143 + CanBeInlined = false; + return node; + } + + public override SyntaxNode VisitSwitchExpression(SwitchExpressionSyntax node) + { + // CS8514 + CanBeInlined = false; + return node; + } + + public override SyntaxNode VisitThrowExpression(ThrowExpressionSyntax node) + { + // CS8188 + CanBeInlined = false; + return node; + } + + private static InvocationExpressionSyntax VisitStaticMethodInvocation(InvocationExpressionSyntax node, IMethodSymbol methodSymbol) + { + var receiverType = FullyQualifiedIdentifier(methodSymbol.ReceiverType!); + var expression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, receiverType, IdentifierName(methodSymbol.Name)); + return node.WithExpression(expression); + } + + private InvocationExpressionSyntax VisitExtensionMethodInvocation(InvocationExpressionSyntax node, IMethodSymbol methodSymbol) + { + if (node.Expression is not MemberAccessExpressionSyntax memberAccess) + return node; + + var receiverArgument = (ExpressionSyntax)Visit(memberAccess.Expression); + var arguments = (ArgumentListSyntax)Visit(node.ArgumentList); + + var args = new List(arguments.Arguments.Count + 1); + args.Add(Argument(receiverArgument.WithoutTrivia())); + args.AddRange(arguments.Arguments); + + var extensionMethodContainingType = FullyQualifiedIdentifier(methodSymbol.ReducedFrom!.ReceiverType!); + var expression = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + extensionMethodContainingType, + IdentifierName(methodSymbol.Name) + ); + return node.Update(expression, ArgumentList(CommaSeparatedList(args))); + } + + private IdentifierNameSyntax? FullyQualifiedParentType(TypeSyntax typeSyntax) + { + var parent = typeSyntax.Parent; + if (parent == null) + return null; + + var type = semanticModel.GetTypeInfo(parent).Type; + return type != null ? FullyQualifiedIdentifier(type).WithTriviaFrom(typeSyntax) : null; + } + + private IdentifierNameSyntax? FullyQualifiedType(TypeSyntax typeSyntax) + { + var type = semanticModel.GetTypeInfo(typeSyntax).Type; + return type != null ? FullyQualifiedIdentifier(type).WithTriviaFrom(typeSyntax) : null; + } +} diff --git a/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs b/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs new file mode 100644 index 0000000000..49695e2206 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs @@ -0,0 +1,82 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.UserMappings; + +namespace Riok.Mapperly.Descriptors; + +public class InlinedExpressionMappingCollection +{ + // use the MappingCollection to re-use + // the mapping default logic. + // Added mapping bodies are never built + // and method mappings are not added to the generated mapper. + private readonly MappingCollection _delegate = new(); + + private readonly Dictionary _namedInlinedMappings = new(); + private readonly Dictionary _inlinedMappings = new(); + + public INewInstanceMapping? FindNamed(string name, out bool ambiguousName, out bool isInlined) + { + if (_namedInlinedMappings.TryGetValue(name, out var inlinedMapping)) + { + isInlined = true; + + // FindNamed was at least once called for this name + // therefore a diagnostic should have been reported already + // if this is an ambiguous name + ambiguousName = false; + return inlinedMapping; + } + + isInlined = false; + return _delegate.FindNamedNewInstanceMapping(name, out ambiguousName); + } + + public INewInstanceMapping? Find(TypeMappingKey mappingKey, out bool isInlined) + { + if (_inlinedMappings.TryGetValue(mappingKey, out var inlinedMapping)) + { + isInlined = true; + return inlinedMapping; + } + + isInlined = false; + return _delegate.FindNewInstanceMapping(mappingKey); + } + + public INewInstanceUserMapping? FindNewInstanceUserMapping(IMethodSymbol method, out bool isInlined) + { + var mapping = _delegate.FindNewInstanceUserMapping(method); + if (mapping == null) + { + isInlined = false; + return null; + } + + if (_inlinedMappings.TryGetValue(new TypeMappingKey(mapping), out var inlinedMapping)) + { + isInlined = true; + return inlinedMapping as INewInstanceUserMapping; + } + + isInlined = false; + return mapping; + } + + public void AddMapping(INewInstanceMapping mapping, TypeMappingConfiguration config) + { + var result = _delegate.AddNewInstanceMapping(mapping, config); + + // only set it as inlined if it was added as default mapping + if (result == MappingCollectionAddResult.Added) + { + SetInlinedMapping(new TypeMappingKey(mapping, config), mapping); + } + } + + public void SetInlinedMapping(TypeMappingKey mappingKey, INewInstanceMapping mapping) => _inlinedMappings[mappingKey] = mapping; + + public void SetInlinedMapping(string name, INewInstanceMapping mapping) => _namedInlinedMappings[name] = mapping; + + public void AddUserMapping(IUserMapping mapping, string? name) => _delegate.AddUserMapping(mapping, name); +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs new file mode 100644 index 0000000000..00a25b2dff --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.UserMappings; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Descriptors.MappingBuilders; + +public static class InlineExpressionMappingBuilder +{ + /// + /// Tries to inline a given mapping. + /// This works as long as the user implemented methods + /// follow the expression tree limitations: + /// https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations + /// + /// The context. + /// The mapping. + /// The inlined mapping or null if it could not be inlined. + public static INewInstanceMapping? TryBuildMapping(InlineExpressionMappingBuilderContext ctx, UserImplementedMethodMapping mapping) + { + if (mapping.Method.DeclaringSyntaxReferences is not [var methodSyntaxRef]) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); + return null; + } + + var methodSyntax = methodSyntaxRef.GetSyntax(); + if (methodSyntax is not MethodDeclarationSyntax { ExpressionBody: { } body, ParameterList.Parameters: [var sourceParameter] }) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); + return null; + } + + var semanticModel = ctx.Compilation.GetSemanticModel(methodSyntax.SyntaxTree); + var inlineRewriter = new InlineExpressionRewriter(semanticModel, ctx.FindNewInstanceMapping); + var bodyExpression = (ExpressionSyntax?)body.Expression.Accept(inlineRewriter); + if (bodyExpression == null || !inlineRewriter.CanBeInlined) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); + return null; + } + + return new UserImplementedInlinedExpressionMapping(mapping, sourceParameter, inlineRewriter.MappingInvocations, bodyExpression); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs index 7084a2f9cf..d664e147a5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Descriptors.MappingBuilders; public class MappingBuilder(MappingCollection mappings, MapperDeclaration mapperDeclaration) { - private HashSet _resolvedMappingNames = new(); + private readonly HashSet _resolvedMappingNames = new(); private delegate INewInstanceMapping? BuildMapping(MappingBuilderContext context); diff --git a/src/Riok.Mapperly/Descriptors/MappingCollection.cs b/src/Riok.Mapperly/Descriptors/MappingCollection.cs index 56fd142c03..3ad0844519 100644 --- a/src/Riok.Mapperly/Descriptors/MappingCollection.cs +++ b/src/Riok.Mapperly/Descriptors/MappingCollection.cs @@ -55,6 +55,8 @@ public class MappingCollection public INewInstanceMapping? FindNewInstanceMapping(TypeMappingKey mappingKey) => _newInstanceMappings.Find(mappingKey); + public INewInstanceUserMapping? FindNewInstanceUserMapping(IMethodSymbol method) => _newInstanceMappings.FindUserMapping(method); + public INewInstanceMapping? FindNamedNewInstanceMapping(string name, out bool ambiguousName) => _newInstanceMappings.FindNamed(name, out ambiguousName); @@ -64,7 +66,7 @@ public class MappingCollection public void EnqueueToBuildBody(ITypeMapping mapping, MappingBuilderContext ctx) => _mappingsToBuildBody.Enqueue((mapping, ctx)); - public MappingCollectionAddResult AddUserMapping(IUserMapping userMapping, bool ignoreDuplicates, string? name) + public MappingCollectionAddResult AddUserMapping(IUserMapping userMapping, string? name) { _userMappings.Add(userMapping); @@ -76,62 +78,50 @@ public MappingCollectionAddResult AddUserMapping(IUserMapping userMapping, bool return userMapping switch { INewInstanceUserMapping newInstanceMapping - => _newInstanceMappings.AddUserMapping(newInstanceMapping, ignoreDuplicates, userMapping.Default, name), + => _newInstanceMappings.AddUserMapping(newInstanceMapping, userMapping.Default, name), IExistingTargetUserMapping existingTargetMapping - => _existingTargetMappings.AddUserMapping(existingTargetMapping, ignoreDuplicates, userMapping.Default, name), + => _existingTargetMappings.AddUserMapping(existingTargetMapping, userMapping.Default, name), _ => throw new ArgumentOutOfRangeException(nameof(userMapping), userMapping.GetType().FullName + " mappings are not supported") }; } - public void AddMapping(ITypeMapping mapping, TypeMappingConfiguration config) - { - switch (mapping) - { - case INewInstanceMapping newInstanceMapping: - AddNewInstanceMapping(newInstanceMapping, config); - break; - - case IExistingTargetMapping existingTargetMapping: - AddExistingTargetMapping(existingTargetMapping, config); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(mapping), mapping.GetType().FullName + " mappings are not supported"); - } - } - - public void AddNewInstanceMapping(INewInstanceMapping mapping, TypeMappingConfiguration config) + public MappingCollectionAddResult AddNewInstanceMapping(INewInstanceMapping mapping, TypeMappingConfiguration config) { if (mapping is MethodMapping methodMapping) { _methodMappings.Add(methodMapping); } - _newInstanceMappings.TryAddAsDefault(mapping, config); + return _newInstanceMappings.TryAddAsDefault(mapping, config); } - public void AddExistingTargetMapping(IExistingTargetMapping mapping, TypeMappingConfiguration config) + public MappingCollectionAddResult AddExistingTargetMapping(IExistingTargetMapping mapping, TypeMappingConfiguration config) { if (mapping is MethodMapping methodMapping) { _methodMappings.Add(methodMapping); } - _existingTargetMappings.TryAddAsDefault(mapping, config); + return _existingTargetMappings.TryAddAsDefault(mapping, config); } public void AddNamedNewInstanceUserMappings(string name, IEnumerable mappings) { foreach (var mapping in mappings) { - Debug.Assert( - mapping.Default != true, - $"Cannot add a named mapping ({name}, {mapping.Method.Name}) after the initial discovery which is a default mapping" - ); - _newInstanceMappings.AddNamedUserMapping(name, mapping); + AddNamedNewInstanceUserMapping(name, mapping); } } + public void AddNamedNewInstanceUserMapping(string name, INewInstanceUserMapping mapping) + { + Debug.Assert( + mapping.Default != true, + $"Cannot add a named mapping ({name}, {mapping.Method.Name}) after the initial discovery which is a default mapping" + ); + _newInstanceMappings.AddNamedUserMapping(name, mapping); + } + private class MappingCollectionInstance where T : ITypeMapping where TUserMapping : T, IUserMapping @@ -144,9 +134,9 @@ private class MappingCollectionInstance private readonly Dictionary _defaultMappings = new(); /// - /// Registered user mapping methods. + /// Registered user mappings by their methods. /// - private readonly HashSet _knownUserMappingMethods = new(SymbolEqualityComparer.Default); + private readonly Dictionary _userMappingsByMethod = new(SymbolEqualityComparer.Default); /// /// Named mappings by their names. @@ -170,7 +160,7 @@ private class MappingCollectionInstance private readonly ListDictionary _duplicatedNonDefaultUserMappings = new(); /// - /// All mapping keys for which was called and returned a non-null result. + /// All mapping keys for which was called and returned a non-null result. /// private readonly HashSet _usedMappingKeys = new(); @@ -186,6 +176,8 @@ private class MappingCollectionInstance public IEnumerable UsedDuplicatedNonDefaultNonReferencedUserMappings => _usedMappingKeys.SelectMany(_duplicatedNonDefaultUserMappings.GetOrEmpty).Where(x => !_referencedNamedMappings.Contains(x)); + public TUserMapping? FindUserMapping(IMethodSymbol method) => _userMappingsByMethod.GetValueOrDefault(method); + public T? Find(TypeMappingKey mappingKey) { if (_defaultMappings.TryGetValue(mappingKey, out var mapping)) @@ -210,10 +202,11 @@ private class MappingCollectionInstance public void AddNamedUserMapping(string? name, TUserMapping mapping) { + var isNewUserMappingMethod = _userMappingsByMethod.TryAdd(mapping.Method, mapping); + if (name == null) return; - var isNewUserMappingMethod = _knownUserMappingMethods.Add(mapping.Method); if (_namedMappings.TryAdd(name, mapping)) return; @@ -229,14 +222,12 @@ public void AddNamedUserMapping(string? name, TUserMapping mapping) public MappingCollectionAddResult TryAddAsDefault(T mapping, TypeMappingConfiguration config) { var mappingKey = new TypeMappingKey(mapping, config); - if (_defaultMappings.ContainsKey(mappingKey)) - return MappingCollectionAddResult.NotAddedDuplicated; - - _defaultMappings[mappingKey] = mapping; - return MappingCollectionAddResult.Added; + return _defaultMappings.TryAdd(mappingKey, mapping) + ? MappingCollectionAddResult.Added + : MappingCollectionAddResult.NotAddedDuplicated; } - public MappingCollectionAddResult AddUserMapping(TUserMapping mapping, bool ignoreDuplicates, bool? isDefault, string? name) + public MappingCollectionAddResult AddUserMapping(TUserMapping mapping, bool? isDefault, string? name) { AddNamedUserMapping(name, mapping); @@ -248,24 +239,19 @@ public MappingCollectionAddResult AddUserMapping(TUserMapping mapping, bool igno // if a default mapping was already added for this type-pair // ignore the current one and return duplicated // otherwise overwrite the existing with the new default one. - true => AddDefaultUserMapping(mapping, ignoreDuplicates), + true => AddDefaultUserMapping(mapping), // no default value specified // add it if none exists yet - null => TryAddUserMappingAsDefault(mapping, ignoreDuplicates) + null => TryAddUserMappingAsDefault(mapping) }; } - private MappingCollectionAddResult TryAddUserMappingAsDefault(TUserMapping mapping, bool ignoreDuplicates) + private MappingCollectionAddResult TryAddUserMappingAsDefault(TUserMapping mapping) { var addResult = TryAddAsDefault(mapping, TypeMappingConfiguration.Default); var mappingKey = new TypeMappingKey(mapping); - if (ignoreDuplicates && addResult == MappingCollectionAddResult.NotAddedDuplicated) - { - addResult = MappingCollectionAddResult.NotAddedIgnored; - } - // the mapping was not added due to it being a duplicate, // there is no default mapping declared (yet) // and no duplicate is registered yet @@ -274,8 +260,9 @@ private MappingCollectionAddResult TryAddUserMappingAsDefault(TUserMapping mappi // are registered for the same type-pair without any default mapping. if ( addResult == MappingCollectionAddResult.NotAddedDuplicated + && !mapping.IsExternal + && !mapping.Default.HasValue && !_explicitDefaultMappingKeys.Contains(mappingKey) - && !_duplicatedNonDefaultUserMappings.ContainsKey(mappingKey) ) { _duplicatedNonDefaultUserMappings.Add(mappingKey, mapping); @@ -284,13 +271,11 @@ private MappingCollectionAddResult TryAddUserMappingAsDefault(TUserMapping mappi return addResult; } - private MappingCollectionAddResult AddDefaultUserMapping(T mapping, bool ignoreDuplicates) + private MappingCollectionAddResult AddDefaultUserMapping(T mapping) { var mappingKey = new TypeMappingKey(mapping); if (!_explicitDefaultMappingKeys.Add(mappingKey)) - { - return ignoreDuplicates ? MappingCollectionAddResult.NotAddedIgnored : MappingCollectionAddResult.NotAddedDuplicatedDefault; - } + return MappingCollectionAddResult.NotAddedDuplicated; _duplicatedNonDefaultUserMappings.Remove(mappingKey); _defaultMappings[mappingKey] = mapping; diff --git a/src/Riok.Mapperly/Descriptors/MappingCollectionAddResult.cs b/src/Riok.Mapperly/Descriptors/MappingCollectionAddResult.cs index 76bd064e65..de2f82407c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingCollectionAddResult.cs +++ b/src/Riok.Mapperly/Descriptors/MappingCollectionAddResult.cs @@ -5,5 +5,4 @@ public enum MappingCollectionAddResult Added, NotAddedIgnored, NotAddedDuplicated, - NotAddedDuplicatedDefault, } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs index 3cc67d8884..fbb29f9ba4 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs @@ -12,4 +12,10 @@ public interface IUserMapping : ITypeMapping /// bool? Default { get; } + + /// + /// An external mapping is defined in another class. + /// E.g. base class or imported via + /// + bool IsExternal { get; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 48e9d20e67..faa94842ee 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -26,6 +26,8 @@ bool enableReferenceHandling public bool? Default => false; + public bool IsExternal => false; + private MethodParameter TargetParameter { get; } = targetParameter; /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 681e947234..8dbcc7426b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -21,6 +21,8 @@ bool enableReferenceHandling public bool? Default { get; } = isDefault; + public bool IsExternal => false; + public INewInstanceMapping? DelegateMapping { get; private set; } /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index a27bbdc6ea..1259ec0fa1 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -36,6 +36,8 @@ ITypeSymbol objectType /// public bool? Default => false; + public bool IsExternal => false; + /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs index 8765e86703..cd65a15e4b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs @@ -16,13 +16,16 @@ public class UserImplementedExistingTargetMethodMapping( bool? isDefault, MethodParameter sourceParameter, MethodParameter targetParameter, - MethodParameter? referenceHandlerParameter + MethodParameter? referenceHandlerParameter, + bool isExternal ) : ExistingTargetMapping(method.Parameters[0].Type, targetParameter.Type), IExistingTargetUserMapping { public IMethodSymbol Method { get; } = method; public bool? Default { get; } = isDefault; + public bool IsExternal { get; } = isExternal; + public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax target) { // if the user implemented method is on an interface, diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs new file mode 100644 index 0000000000..6e54214687 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs @@ -0,0 +1,68 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; + +/// +/// An inlined version of a . +/// Does not support reference handling and has several other limitations, +/// . +/// +/// The original user mapping. +/// The source parameter of the user mapping. This will probably be rewritten when inlining. +/// Mapping invocations to be inlined. +/// The prepared user written mapping body code (rewritten by . +public class UserImplementedInlinedExpressionMapping( + UserImplementedMethodMapping userMapping, + ParameterSyntax sourceParameter, + IReadOnlyDictionary mappingInvocations, + ExpressionSyntax mappingBody +) : NewInstanceMapping(userMapping.SourceType, userMapping.TargetType), INewInstanceMapping +{ + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + { + var body = InlineUserMappings(ctx, mappingBody); + return ReplaceSource(ctx, body); + } + + private ExpressionSyntax InlineUserMappings(TypeMappingBuildContext ctx, ExpressionSyntax body) + { + var invocations = body.GetAnnotatedNodes(InlineExpressionRewriter.SyntaxAnnotationKindMapperInvocation) + .OfType(); + return body.ReplaceNodes(invocations, (invocation, _) => InlineMapping(ctx, invocation)); + } + + private ExpressionSyntax InlineMapping(TypeMappingBuildContext ctx, InvocationExpressionSyntax invocation) + { + var annotation = invocation.GetAnnotations(InlineExpressionRewriter.SyntaxAnnotationKindMapperInvocation).FirstOrDefault(); + if (!mappingInvocations.TryGetValue(annotation, out var mapping)) + return invocation; + + return mapping.Build(ctx.WithSource(invocation.ArgumentList.Arguments[0].Expression)); + } + + private ExpressionSyntax ReplaceSource(TypeMappingBuildContext ctx, ExpressionSyntax body) + { + // include self since the method could just be TTarget MyMapping(TSource source) => source; + // do not further descend if the source parameter is hidden + var identifierNodes = body.DescendantNodesAndSelf(n => !IsSourceParameterHidden(n)) + .OfType() + .Where(x => x.Identifier.Text.Equals(sourceParameter.Identifier.Text, StringComparison.Ordinal)); + return body.ReplaceNodes(identifierNodes, (n, _) => ctx.Source.WithTriviaFrom(n)); + } + + private bool IsSourceParameterHidden(SyntaxNode node) + { + return ExtractOverwrittenIdentifiers(node).Any(x => x.Text.Equals(sourceParameter.Identifier.Text, StringComparison.Ordinal)); + } + + private IEnumerable ExtractOverwrittenIdentifiers(SyntaxNode node) + { + return node switch + { + SimpleLambdaExpressionSyntax simpleLambda => new[] { simpleLambda.Parameter.Identifier }, + ParenthesizedLambdaExpressionSyntax lambda => lambda.ParameterList.Parameters.Select(p => p.Identifier), + _ => Enumerable.Empty() + }; + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs index fa0a78cbfe..5adf0e3e5c 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs @@ -15,12 +15,14 @@ public class UserImplementedMethodMapping( bool? isDefault, MethodParameter sourceParameter, ITypeSymbol targetType, - MethodParameter? referenceHandlerParameter + MethodParameter? referenceHandlerParameter, + bool isExternal ) : NewInstanceMapping(sourceParameter.Type, targetType), INewInstanceUserMapping { public IMethodSymbol Method { get; } = method; public bool? Default { get; } = isDefault; + public bool IsExternal { get; } = isExternal; public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 2bc9a3c0c0..3e613cbc06 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -20,6 +20,7 @@ public class SimpleMappingBuilderContext( DiagnosticCollection diagnostics, MappingBuilder mappingBuilder, ExistingTargetMappingBuilder existingTargetMappingBuilder, + InlinedExpressionMappingCollection inlinedMappings, Location diagnosticLocation ) { @@ -38,6 +39,7 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? ctx._diagnostics, ctx.MappingBuilder, ctx.ExistingTargetMappingBuilder, + ctx.InlinedMappings, diagnosticLocation ?? ctx._diagnosticLocation ) { } @@ -57,6 +59,13 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? protected ExistingTargetMappingBuilder ExistingTargetMappingBuilder { get; } = existingTargetMappingBuilder; + /// + /// The inline expression mappings. + /// Note: No method mappings should be added to this collection + /// and the body of these mappings is never built. + /// + protected InlinedExpressionMappingCollection InlinedMappings { get; } = inlinedMappings; + public virtual bool IsConversionEnabled(MappingConversionType conversionType) => Configuration.Mapper.EnabledConversions.HasFlag(conversionType); diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index e29b384015..13282c4dd0 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -21,7 +21,14 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild { var mapping = BuilderUserDefinedMapping(ctx, method) - ?? BuildUserImplementedMapping(ctx, method, receiver: null, allowPartial: false, isStatic: mapperSymbol.IsStatic); + ?? BuildUserImplementedMapping( + ctx, + method, + receiver: null, + allowPartial: false, + isStatic: mapperSymbol.IsStatic, + isExternal: false + ); if (mapping != null) yield return mapping; } @@ -39,7 +46,7 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild baseAndInterfaceMethods = baseAndInterfaceMethods.Distinct(SymbolTypeEqualityComparer.MethodDefault); - foreach (var mapping in BuildUserImplementedMappings(ctx, baseAndInterfaceMethods, null, false)) + foreach (var mapping in BuildUserImplementedMappings(ctx, baseAndInterfaceMethods, null, isStatic: false, isExternal: true)) { yield return mapping; } @@ -55,7 +62,7 @@ string name .GetMembers(name) .OfType() .Where(m => IsMappingMethodCandidate(ctx, m, requireAttribute: false)) - .Select(m => BuildUserImplementedMapping(ctx, m, null, allowPartial: true, isStatic: mapperSymbol.IsStatic)) + .Select(m => BuildUserImplementedMapping(ctx, m, null, allowPartial: true, isStatic: mapperSymbol.IsStatic, isExternal: false)) .OfType(); } @@ -63,21 +70,23 @@ internal static IEnumerable ExtractUserImplementedMappings( SimpleMappingBuilderContext ctx, ITypeSymbol type, string? receiver, - bool isStatic + bool isStatic, + bool isExternal ) { var methods = ctx .SymbolAccessor.GetAllMethods(type) .Concat(type.AllInterfaces.SelectMany(ctx.SymbolAccessor.GetAllMethods)) .Distinct(SymbolTypeEqualityComparer.MethodDefault); - return BuildUserImplementedMappings(ctx, methods, receiver, isStatic); + return BuildUserImplementedMappings(ctx, methods, receiver, isStatic, isExternal); } private static IEnumerable BuildUserImplementedMappings( SimpleMappingBuilderContext ctx, IEnumerable methods, string? receiver, - bool isStatic + bool isStatic, + bool isExternal ) { foreach (var method in methods) @@ -89,7 +98,7 @@ bool isStatic // but still treated as user implemented methods, // since the user should provide an implementation elsewhere. // This is the case if a partial mapper class is extended. - var mapping = BuildUserImplementedMapping(ctx, method, receiver, true, isStatic); + var mapping = BuildUserImplementedMapping(ctx, method, receiver, true, isStatic, isExternal); if (mapping != null) yield return mapping; } @@ -115,7 +124,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM IMethodSymbol method, string? receiver, bool allowPartial, - bool isStatic + bool isStatic, + bool isExternal ) { var userMappingConfig = GetUserMappingConfig(ctx, method, out var hasAttribute); @@ -143,7 +153,8 @@ bool isStatic userMappingConfig.Default, parameters.Source, parameters.Target!.Value, - parameters.ReferenceHandler + parameters.ReferenceHandler, + isExternal ); } @@ -153,7 +164,8 @@ bool isStatic userMappingConfig.Default, parameters.Source, ctx.SymbolAccessor.UpgradeNullable(method.ReturnType), - parameters.ReferenceHandler + parameters.ReferenceHandler, + isExternal ); } diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index a05ccced7b..a790c1f908 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -657,6 +657,16 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor QueryableProjectionMappingCannotInline = + new( + "RMG068", + "Cannot inline user implemented queryable expression mapping", + "Cannot inline user implemented queryable expression mapping", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Info, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/LongValueDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/LongValueDto.cs new file mode 100644 index 0000000000..48fc89d01c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Dto/LongValueDto.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.IntegrationTests.Dto +{ + public class LongValueDto + { + public long Value { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDtoProjection.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDtoProjection.cs index ee271d843f..4615ded0ac 100644 --- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDtoProjection.cs +++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDtoProjection.cs @@ -67,5 +67,9 @@ public TestObjectDtoProjection(int ctorValue) public int ManuallyMappedModified { get; set; } public List ManuallyMappedList { get; set; } = new(); + + public ICollection IntegerValues { get; set; } = new List(); + + public ICollection DecimalValues { get; set; } = new List(); } } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs index bbfa373040..39cf9a78ae 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Riok.Mapperly.Abstractions; using Riok.Mapperly.IntegrationTests.Dto; @@ -25,10 +26,7 @@ public static partial class ProjectionMapper private static partial TestObjectDtoProjection ProjectToDto(this TestObjectProjection testObject); [UserMapping] - private static TestObjectDtoManuallyMappedProjection? MapManual(string str) - { - return new TestObjectDtoManuallyMappedProjection(100) { StringValue = str, }; - } + private static TestObjectDtoManuallyMappedProjection? MapManual(string str) => new(100) { StringValue = str }; [UserMapping] private static TestEnum MapManual(TestObjectProjectionEnumValue source) => source.Value; @@ -37,6 +35,17 @@ public static partial class ProjectionMapper [MapDerivedType(typeof(TestObjectProjectionTypeB), typeof(TestObjectDtoProjectionTypeB))] private static partial TestObjectDtoProjectionBaseType MapDerived(TestObjectProjectionBaseType source); + [UserMapping] + private static ICollection OrderIntegerValues(ICollection values) => + values.OrderBy(x => x.Value).ToList(); + + [UserMapping] + private static ICollection OrderAndMapLongValues(ICollection values) => + values.OrderBy(x => x.Value).Select(x => MapLongValue(x)).ToList(); + + [MapperIgnoreSource(nameof(LongValue.Id))] + private static partial LongValueDto MapLongValue(LongValue value); + private static int ModifyInt(int v) => v + 10; } } diff --git a/test/Riok.Mapperly.IntegrationTests/Models/IntegerValue.cs b/test/Riok.Mapperly.IntegrationTests/Models/IntegerValue.cs new file mode 100644 index 0000000000..0cebc25454 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/IntegerValue.cs @@ -0,0 +1,9 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class IntegerValue + { + public int Id { get; set; } + + public int Value { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Models/LongValue.cs b/test/Riok.Mapperly.IntegrationTests/Models/LongValue.cs new file mode 100644 index 0000000000..39acc913ef --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/LongValue.cs @@ -0,0 +1,9 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class LongValue + { + public int Id { get; set; } + + public long Value { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObjectProjection.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObjectProjection.cs index 4d5892de81..9fe8e97f8d 100644 --- a/test/Riok.Mapperly.IntegrationTests/Models/TestObjectProjection.cs +++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObjectProjection.cs @@ -68,5 +68,9 @@ public class TestObjectProjection public int ManuallyMappedModified { get; set; } public List ManuallyMappedList { get; set; } = new(); + + public ICollection IntegerValues { get; set; } = new List(); + + public ICollection DecimalValues { get; set; } = new List(); } } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery.verified.sql index d62c2d48cb..a144d73698 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", "o"."DateTimeValueTargetDateOnly", "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified", "t3"."Id", "t3"."OtherValue", "t3"."TestObjectProjectionId", "t3"."Value" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", "o"."DateTimeValueTargetDateOnly", "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" @@ -12,4 +12,6 @@ LEFT JOIN "InheritanceSubObject" AS "i1" ON "o"."SubObjectSubIntValue" = "i1"."S LEFT JOIN "TestObjectNested" AS "t1" ON "o"."Id" = "t1"."TestObjectProjectionId" LEFT JOIN "TestObjectNested" AS "t2" ON "o"."Id" = "t2"."TestObjectProjectionId" LEFT JOIN "TestObjectProjectionEnumValue" AS "t3" ON "o"."Id" = "t3"."TestObjectProjectionId" -ORDER BY "o"."Id", "i"."IdValue", "i0"."IdValue", "t"."IntValue", "t0"."IntValue", "o0"."Id", "i1"."SubIntValue", "t1"."IntValue", "t2"."IntValue" \ No newline at end of file +LEFT JOIN "IntegerValue" AS "i2" ON "o"."Id" = "i2"."TestObjectProjectionId" +LEFT JOIN "LongValue" AS "l" ON "o"."Id" = "l"."TestObjectProjectionId" +ORDER BY "o"."Id", "i"."IdValue", "i0"."IdValue", "t"."IntValue", "t0"."IntValue", "o0"."Id", "i1"."SubIntValue", "t1"."IntValue", "t2"."IntValue", "t3"."Id", "i2"."Value", "i2"."Id", "l"."Value" \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql index 434f59d591..e43875a957 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified", "t3"."Id", "t3"."OtherValue", "t3"."TestObjectProjectionId", "t3"."Value" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" @@ -12,4 +12,6 @@ LEFT JOIN "InheritanceSubObject" AS "i1" ON "o"."SubObjectSubIntValue" = "i1"."S LEFT JOIN "TestObjectNested" AS "t1" ON "o"."Id" = "t1"."TestObjectProjectionId" LEFT JOIN "TestObjectNested" AS "t2" ON "o"."Id" = "t2"."TestObjectProjectionId" LEFT JOIN "TestObjectProjectionEnumValue" AS "t3" ON "o"."Id" = "t3"."TestObjectProjectionId" -ORDER BY "o"."Id", "i"."IdValue", "i0"."IdValue", "t"."IntValue", "t0"."IntValue", "o0"."Id", "i1"."SubIntValue", "t1"."IntValue", "t2"."IntValue" \ No newline at end of file +LEFT JOIN "IntegerValue" AS "i2" ON "o"."Id" = "i2"."TestObjectProjectionId" +LEFT JOIN "LongValue" AS "l" ON "o"."Id" = "l"."TestObjectProjectionId" +ORDER BY "o"."Id", "i"."IdValue", "i0"."IdValue", "t"."IntValue", "t0"."IntValue", "o0"."Id", "i1"."SubIntValue", "t1"."IntValue", "t2"."IntValue", "t3"."Id", "i2"."Value", "i2"."Id", "l"."Value" \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs index a1ed325952..551d6dccd1 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,4 +1,4 @@ -// +// #nullable enable namespace Riok.Mapperly.IntegrationTests.Mapper { @@ -44,9 +44,14 @@ public static partial class ProjectionMapper } : default, DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(x.DateTimeValueTargetDateOnly), DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(x.DateTimeValueTargetTimeOnly), - ManuallyMapped = MapManual(x.ManuallyMapped), - ManuallyMappedModified = ModifyInt(x.ManuallyMappedModified), - ManuallyMappedList = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.ManuallyMappedList, x1 => MapManual(x1))), + ManuallyMapped = new(100) { StringValue = x.ManuallyMapped }, + ManuallyMappedModified = x.ManuallyMappedModified + 10, + ManuallyMappedList = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.ManuallyMappedList, x1 => x1.Value)), + IntegerValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.OrderBy(x.IntegerValues, x => x.Value)), + DecimalValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.OrderBy(x.DecimalValues, x => x.Value), x => new global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto() + { + Value = x.Value, + })), }); #nullable enable } @@ -126,6 +131,8 @@ public static partial class ProjectionMapper target.ManuallyMapped = MapManual(testObject.ManuallyMapped); target.ManuallyMappedModified = ModifyInt(testObject.ManuallyMappedModified); target.ManuallyMappedList = MapToListOfTestEnum(testObject.ManuallyMappedList); + target.IntegerValues = OrderIntegerValues(testObject.IntegerValues); + target.DecimalValues = OrderAndMapLongValues(testObject.DecimalValues); return target; } @@ -140,6 +147,14 @@ public static partial class ProjectionMapper }; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static partial global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto MapLongValue(global::Riok.Mapperly.IntegrationTests.Models.LongValue value) + { + var target = new global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto(); + target.Value = value.Value; + return target; + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private static global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDtoProjectionTypeA MapToTestObjectDtoProjectionTypeA(global::Riok.Mapperly.IntegrationTests.Models.TestObjectProjectionTypeA source) { diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index 5a5a84e9c6..475c5dfe8f 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -44,9 +44,14 @@ public static partial class ProjectionMapper } : default, DateTimeValueTargetDateOnly = global::System.DateOnly.FromDateTime(x.DateTimeValueTargetDateOnly), DateTimeValueTargetTimeOnly = global::System.TimeOnly.FromDateTime(x.DateTimeValueTargetTimeOnly), - ManuallyMapped = MapManual(x.ManuallyMapped), - ManuallyMappedModified = ModifyInt(x.ManuallyMappedModified), - ManuallyMappedList = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.ManuallyMappedList, x1 => MapManual(x1))), + ManuallyMapped = new(100) { StringValue = x.ManuallyMapped }, + ManuallyMappedModified = x.ManuallyMappedModified + 10, + ManuallyMappedList = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.ManuallyMappedList, x1 => x1.Value)), + IntegerValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.OrderBy(x.IntegerValues, x => x.Value)), + DecimalValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.OrderBy(x.DecimalValues, x => x.Value), x => new global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto() + { + Value = x.Value, + })), }); #nullable enable } @@ -126,6 +131,8 @@ public static partial class ProjectionMapper target.ManuallyMapped = MapManual(testObject.ManuallyMapped); target.ManuallyMappedModified = ModifyInt(testObject.ManuallyMappedModified); target.ManuallyMappedList = MapToListOfTestEnum(testObject.ManuallyMappedList); + target.IntegerValues = OrderIntegerValues(testObject.IntegerValues); + target.DecimalValues = OrderAndMapLongValues(testObject.DecimalValues); return target; } @@ -140,6 +147,14 @@ public static partial class ProjectionMapper }; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static partial global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto MapLongValue(global::Riok.Mapperly.IntegrationTests.Models.LongValue value) + { + var target = new global::Riok.Mapperly.IntegrationTests.Dto.LongValueDto(); + target.Value = value.Value; + return target; + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private static global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDtoProjectionTypeA MapToTestObjectDtoProjectionTypeA(global::Riok.Mapperly.IntegrationTests.Models.TestObjectProjectionTypeA source) { diff --git a/test/Riok.Mapperly.Tests/Helpers/InlineExpressionRewriterTest.cs b/test/Riok.Mapperly.Tests/Helpers/InlineExpressionRewriterTest.cs new file mode 100644 index 0000000000..80bcfb0f24 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Helpers/InlineExpressionRewriterTest.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; + +namespace Riok.Mapperly.Tests.Helpers; + +public class InlineExpressionRewriterTest +{ + [Theory] + [InlineData("source + \"fooBar\"")] + [InlineData( + "new string(source.Select(char.ToUpper).ToArray())", + true, + "new string(global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(source, char.ToUpper)))" + )] + [InlineData("typeof(Test).ToString()", true, "typeof(global::System.Type).ToString()")] + [InlineData("FooBar(source)", true, "global::Test.FooBar(source)")] + [InlineData("OtherTest.FooBar(source)", true, "global::OtherNamespace.OtherTest.FooBar(source)")] + [InlineData("new List().ToString()", true, "new List().ToString()")] + [InlineData("new TestRecord[2].ToString()", true, "new global::TestRecord[2].ToString()")] + [InlineData("base.ToString()", false)] // CS0831 + [InlineData("(1,2).ToString()", false)] // CS8143 + [InlineData("((string?)source)?.ToString()!", false)] // CS8072 + [InlineData("source switch { _ => \"fooBar\" }", false)] // CS8514 + [InlineData("throw new Exception()", false)] // CS8188 + [InlineData( + "(new TestRecord(10) with { Value = 20 }).ToString()", + false, + "(new global::TestRecord(10) with { Value = 20 }).ToString()" + )] // CS8849 + [InlineData("new List()[1..3].ToString()", false)] // CS8792 + [InlineData("((List) [1,2,3]).ToString()", false)] // CS9175 + public void RewriteExpression(string expression, bool canBeInlined = true, string? inlinedExpression = null) + { + var (result, inlineOk) = Rewrite( + $$""" + using System; + using System.Linq; + using OtherNamespace; + + public class Test + { + public string MapExpression(string source) + => {{expression}}; + + private static string FooBar(string s) + => s + "fooBar"; + } + + public record TestRecord(int Value); + + namespace OtherNamespace + { + public class OtherTest + { + public static string FooBar(string s) + => s + "otherFooBar"; + } + } + """ + ); + inlineOk.Should().Be(canBeInlined); + result.Should().Be(inlinedExpression ?? expression); + } + + private (string Result, bool CanBeInlined) Rewrite([StringSyntax(StringSyntax.CSharp)] string source) + { + var compilation = TestHelper.BuildCompilation(source); + var bodyNode = compilation + .SyntaxTrees.Single() + .GetRoot() + .DescendantNodes() + .OfType() + .Single(x => x.Parent is MethodDeclarationSyntax { Identifier.Text: "MapExpression" }); + var model = compilation.GetSemanticModel(bodyNode.SyntaxTree); + var rewriter = new InlineExpressionRewriter(model, _ => null); + var result = rewriter.Visit(bodyNode.Expression); + return (result.ToFullString(), rewriter.CanBeInlined); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs index 4ec7c4d156..88f23f41e0 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs @@ -93,7 +93,7 @@ public Task RecordToRecordManualFlatteningInsideList() """, "record A(Guid Id, List Values);", "record B(string Id, List Values);", - "enum D(V1, V2, V3);", + "record D(int IntValue);", "record C(D Value);" ); @@ -107,7 +107,7 @@ public Task RecordToRecordManualListMapping() """ private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); - private partial List Map(List source) => source.Select(x => x.Value).ToList(); + private partial List Map(List source) => source.Select(x => x.Value).OrderBy(x => x).ToList(); """, "record A(Guid Id, List Values);", "record B(string Id, List Values);", @@ -118,24 +118,6 @@ public Task RecordToRecordManualListMapping() return TestHelper.VerifyGenerator(source); } - [Fact] - public Task ClassToClassWithUserImplemented() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes( - """ - private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); - - private D MapToD(C v) => new D { Value = v.Value + "-mapped" }; - """, - "class A { public string StringValue { get; set; } public C NestedValue { get; set; } }", - "class B { public string StringValue { get; set; } public D NestedValue { get; set; } }", - "class C { public string Value { get; set; } }", - "class D { public string Value { get; set; } }" - ); - - return TestHelper.VerifyGenerator(source); - } - [Fact] public Task ReferenceLoopInitProperty() { diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs index a28d0a766c..6c3e8be1b0 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUseNamedMappingTest.cs @@ -162,4 +162,36 @@ class B ); return TestHelper.VerifyGenerator(source); } + + [Fact] + public Task TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + public partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + [MapProperty(nameof(A.StringValue1), nameof(B.StringValue1), Use = nameof(ModifyString)] + [MapProperty(nameof(A.StringValue2), nameof(B.StringValue2), Use = nameof(ModifyString2)] + private partial B Map(A source); + + [MapProperty(nameof(A.StringValue1), nameof(B.StringValue1), Use = nameof(ModifyString)] + [MapProperty(nameof(A.StringValue2), nameof(B.StringValue2), Use = nameof(ModifyString2)] + private partial D Map(C source); + + private string ModifyString(string source) => source + "-modified"; + private int ModifyString(int value) => value + "-modified-ambiguous"; + private string ModifyString2(string source) => source + "-modified2"; + + [UserMapping] + private string DefaultStringMapping(string source) => source; + """, + TestSourceBuilderOptions.WithDisabledAutoUserMappings, + "record A(string StringValue, string StringValue1, string StringValue2);", + "record B(string StringValue, string StringValue1, string StringValue2);", + "record C(string StringValue, string StringValue1, string StringValue2);", + "record D(string StringValue, string StringValue1, string StringValue2);" + ); + return TestHelper.VerifyGenerator(source); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs new file mode 100644 index 0000000000..34dcb84567 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs @@ -0,0 +1,122 @@ +namespace Riok.Mapperly.Tests.Mapping; + +public class QueryableProjectionUserImplementedTest +{ + [Fact] + public Task ClassToClassInlinedExpression() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private D MapToD(C v) => new D { Value = v.Value + "-mapped" }; + """, + "class A { public string StringValue { get; set; } public C NestedValue { get; set; } }", + "class B { public string StringValue { get; set; } public D NestedValue { get; set; } }", + "class C { public string Value { get; set; } }", + "class D { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ClassToClassNonInlinedMethod() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private D MapToD(C v) + { + return new D { Value = v.Value + "-mapped" }; + } + """, + "class A { public string StringValue { get; set; } public C NestedValue { get; set; } }", + "class B { public string StringValue { get; set; } public D NestedValue { get; set; } }", + "class C { public string Value { get; set; } }", + "class D { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ClassToClassUserImplementedOrdering() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private List Order(List v) + => v.OrderBy(x => x.Value).ToList(); + """, + "class A { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class B { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class C { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ClassToClassUserImplementedParenthesizedLambdaOrdering() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private List Order(List v) + => v.OrderBy((x) => x.Value).ToList(); + """, + "class A { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class B { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class C { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ClassToClassUserImplementedOrderingWithMappingAndParameterHiding() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private partial D MapToD(C source); + + private List Order(List v) + => v.OrderBy(x => x.Value).Select(v => MapToD(v)).ToList(); + """, + "class A { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class B { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class C { public string Value { get; set; } }", + "class D { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ClassToClassUserImplementedOrderingWithTwoNestedMappings() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private partial D MapToD(C source); + private string MapString(string s) => s + "-mod"; + + private List Order(List v) + => v.OrderBy(x => x.Value).Select(x => MapToD(x)).ToList(); + """, + "class A { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class B { public string StringValue { get; set; } public List NestedValues { get; set; } }", + "class C { public string Value { get; set; } }", + "class D { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } +} diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs index 94351ae16f..ae3dc25f27 100644 --- a/test/Riok.Mapperly.Tests/TestHelper.cs +++ b/test/Riok.Mapperly.Tests/TestHelper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -53,6 +54,9 @@ public static MapperGenerationResult GenerateMapper( return mapperResult; } + public static CSharpCompilation BuildCompilation([StringSyntax(StringSyntax.CSharp)] string source) => + BuildCompilation(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default)); + public static CSharpCompilation BuildCompilation(params SyntaxTree[] syntaxTrees) => BuildCompilation("Tests", NullableContextOptions.Enable, true, syntaxTrees); diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 999eacdf38..9d393a31c6 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -46,7 +46,7 @@ public static string MapperWithBody([StringSyntax(StringSyntax.CSharp)] string b using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions.ReferenceHandling; - {{(options.Namespace != null ? $"namespace {options.Namespace};" : string.Empty)}} + {{(options.Namespace != null ? $"namespace {options.Namespace};" : "")}} {{BuildAttribute(options)}} public {{(options.Static ? "static " : "")}}partial class {{options.MapperClassName}}{{( diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualFlatteningInsideList#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualFlatteningInsideList#Mapper.g.verified.cs index 9f965574a1..df250cbe42 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualFlatteningInsideList#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualFlatteningInsideList#Mapper.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: Mapper.g.cs +//HintName: Mapper.g.cs // #nullable enable public partial class Mapper @@ -7,7 +7,7 @@ public partial class Mapper private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) { #nullable disable - return System.Linq.Queryable.Select(source, x => new global::B(x.Id.ToString(), global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.Values, x1 => Map(x1))))); + return System.Linq.Queryable.Select(source, x => new global::B(x.Id.ToString(), global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.Values, x1 => x1.Value)))); #nullable enable } } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualListMapping#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualListMapping#Mapper.g.verified.cs index 4de4f50640..d46efa356a 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualListMapping#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.RecordToRecordManualListMapping#Mapper.g.verified.cs @@ -7,7 +7,7 @@ public partial class Mapper private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) { #nullable disable - return System.Linq.Queryable.Select(source, x => new global::B(x.Id.ToString(), Map(x.Values))); + return System.Linq.Queryable.Select(source, x => new global::B(x.Id.ToString(), global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.OrderBy(global::System.Linq.Enumerable.Select(x.Values, x => x.Value), x => x)))); #nullable enable } } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs index a0b66ad027..49d7ab070d 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs @@ -7,7 +7,10 @@ public partial class Mapper private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) { #nullable disable - return System.Linq.Queryable.Select(source, x => new global::B() + return System.Linq.Queryable.Select(source, x => new global::B(x.Parent != null ? new global::B() + { + IntValue = x.Parent.IntValue, + } : default) { IntValue = x.IntValue, }); diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs index a0b66ad027..27393fd0b2 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs @@ -9,6 +9,10 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { + Parent = x.Parent != null ? new global::B() + { + IntValue = x.Parent.IntValue, + } : default, IntValue = x.IntValue, }); #nullable enable diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedProperties#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedProperties#Mapper.g.verified.cs index ceca632bfd..3c564d278e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedProperties#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedProperties#Mapper.g.verified.cs @@ -9,9 +9,9 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { - StringValue = DefaultStringMapping(x.StringValue), - StringValue1 = ModifyString(x.StringValue1), - StringValue2 = ModifyString2(x.StringValue2), + StringValue = x.StringValue, + StringValue1 = x.StringValue1 + "-modified", + StringValue2 = x.StringValue2 + "-modified2", }); #nullable enable } diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs index ceca632bfd..3c564d278e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs @@ -9,9 +9,9 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { - StringValue = DefaultStringMapping(x.StringValue), - StringValue1 = ModifyString(x.StringValue1), - StringValue2 = ModifyString2(x.StringValue2), + StringValue = x.StringValue, + StringValue1 = x.StringValue1 + "-modified", + StringValue2 = x.StringValue2 + "-modified2", }); #nullable enable } diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedProperties#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedProperties#Mapper.g.verified.cs index 56ef8d695f..67a6ef2ca3 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedProperties#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedProperties#Mapper.g.verified.cs @@ -9,8 +9,8 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { - Value = MapToDWithP(x.Value), - Value2 = MapToDWithC(x.Value2), + Value = new global::D(x.Value.Value.ToString("P")), + Value2 = new global::D(x.Value2.Value.ToString("C")), ValueDefault = new global::D(x.ValueDefault.Value.ToString("N")), }); #nullable enable diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs index 56ef8d695f..67a6ef2ca3 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.ShouldUseReferencedUserDefinedMappingOnSelectedPropertiesWithDisabledAutoUserMappings#Mapper.g.verified.cs @@ -9,8 +9,8 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { - Value = MapToDWithP(x.Value), - Value2 = MapToDWithC(x.Value2), + Value = new global::D(x.Value.Value.ToString("P")), + Value2 = new global::D(x.Value2.Value.ToString("C")), ValueDefault = new global::D(x.ValueDefault.Value.ToString("N")), }); #nullable enable diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappings#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappings#Mapper.g.verified.cs new file mode 100644 index 0000000000..d702a71520 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappings#Mapper.g.verified.cs @@ -0,0 +1,35 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.StringValue, x.StringValue1 + "-modified", x.StringValue2 + "-modified2")); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::D(x.StringValue, x.StringValue1 + "-modified", x.StringValue2 + "-modified2")); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B Map(global::A source) + { + var target = new global::B(DefaultStringMapping(source.StringValue), ModifyString(source.StringValue1), ModifyString2(source.StringValue2)); + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D Map(global::C source) + { + var target = new global::D(DefaultStringMapping(source.StringValue), ModifyString(source.StringValue1), ModifyString2(source.StringValue2)); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName#Mapper.g.verified.cs new file mode 100644 index 0000000000..d702a71520 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName#Mapper.g.verified.cs @@ -0,0 +1,35 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.StringValue, x.StringValue1 + "-modified", x.StringValue2 + "-modified2")); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::D(x.StringValue, x.StringValue1 + "-modified", x.StringValue2 + "-modified2")); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B Map(global::A source) + { + var target = new global::B(DefaultStringMapping(source.StringValue), ModifyString(source.StringValue1), ModifyString2(source.StringValue2)); + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D Map(global::C source) + { + var target = new global::D(DefaultStringMapping(source.StringValue), ModifyString(source.StringValue1), ModifyString2(source.StringValue2)); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt new file mode 100644 index 0000000000..a2b40d8f22 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt @@ -0,0 +1,24 @@ +{ + Diagnostics: [ + { + Id: RMG062, + Title: The referenced mapping name is ambiguous, + Severity: Error, + WarningLevel: 0, + Location: : (14,1)-(14,87), + MessageFormat: The referenced mapping name {0} is ambiguous, use a unique name, + Message: The referenced mapping name ModifyString is ambiguous, use a unique name, + Category: Mapper + }, + { + Id: RMG062, + Title: The referenced mapping name is ambiguous, + Severity: Error, + WarningLevel: 0, + Location: : (14,1)-(14,87), + MessageFormat: The referenced mapping name {0} is ambiguous, use a unique name, + Message: The referenced mapping name ModifyString is ambiguous, use a unique name, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedExpression#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedExpression#Mapper.g.verified.cs new file mode 100644 index 0000000000..6e34c8e85f --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedExpression#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + NestedValue = new global::D { Value = x.NestedValue.Value + "-mapped" }, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod#Mapper.g.verified.cs similarity index 100% rename from test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs rename to test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod#Mapper.g.verified.cs diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod.verified.txt new file mode 100644 index 0000000000..7186af5608 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassNonInlinedMethod.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: RMG068, + Title: Cannot inline user implemented queryable expression mapping, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,84), + MessageFormat: Cannot inline user implemented queryable expression mapping, + Message: Cannot inline user implemented queryable expression mapping, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrdering#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrdering#Mapper.g.verified.cs new file mode 100644 index 0000000000..89bbaa8ecc --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrdering#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + NestedValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.OrderBy(x.NestedValues, x => x.Value)), + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndMethodGroup#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndMethodGroup#Mapper.g.verified.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndParameterHiding#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndParameterHiding#Mapper.g.verified.cs new file mode 100644 index 0000000000..6d9570e12d --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithMappingAndParameterHiding#Mapper.g.verified.cs @@ -0,0 +1,28 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + NestedValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.OrderBy(x.NestedValues, x => x.Value), v => new global::D() + { + Value = v.Value, + })), + }); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D MapToD(global::C source) + { + var target = new global::D(); + target.Value = source.Value; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithTwoNestedMappings#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithTwoNestedMappings#Mapper.g.verified.cs new file mode 100644 index 0000000000..dcc486580c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedOrderingWithTwoNestedMappings#Mapper.g.verified.cs @@ -0,0 +1,28 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue + "-mod", + NestedValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.OrderBy(x.NestedValues, x => x.Value), x => new global::D() + { + Value = x.Value + "-mod", + })), + }); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D MapToD(global::C source) + { + var target = new global::D(); + target.Value = MapString(source.Value); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedParenthesizedLambdaOrdering#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedParenthesizedLambdaOrdering#Mapper.g.verified.cs new file mode 100644 index 0000000000..719800d262 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassUserImplementedParenthesizedLambdaOrdering#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + NestedValues = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.OrderBy(x.NestedValues, (x) => x.Value)), + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassWithUserImplemented#Mapper.g.verified.cs new file mode 100644 index 0000000000..e69de29bb2