diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 27ea19c012..1bc08f3f32 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -15,8 +15,8 @@ namespace Riok.Mapperly.Descriptors; /// public class InlineExpressionMappingBuilderContext : MappingBuilderContext { - public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, TypeMappingKey mappingKey) - : base(ctx, ctx.FindMapping(mappingKey) as IUserMapping, null, mappingKey, false) { } + public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, IUserMapping? userMapping, TypeMappingKey mappingKey) + : base(ctx, userMapping, null, mappingKey, false) { } private InlineExpressionMappingBuilderContext( InlineExpressionMappingBuilderContext ctx, diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs index 6a18aeb121..e98738d584 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs @@ -1,5 +1,7 @@ +using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.UserMappings; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; @@ -12,10 +14,18 @@ public static class QueryableMappingBuilder if (!ctx.IsConversionEnabled(MappingConversionType.Queryable)) return null; - if (!TryBuildMappingKey(ctx, out var mappingKey)) + if (!ctx.Source.ImplementsGeneric(ctx.Types.Get(typeof(IQueryable<>)), out var sourceQueryable)) return null; - var inlineCtx = new InlineExpressionMappingBuilderContext(ctx, mappingKey); + if (!ctx.Target.ImplementsGeneric(ctx.Types.Get(typeof(IQueryable<>)), out var targetQueryable)) + return null; + + var sourceType = sourceQueryable.TypeArguments[0]; + var targetType = targetQueryable.TypeArguments[0]; + + var mappingKey = TryBuildMappingKey(ctx, sourceType, targetType); + var userMapping = ctx.FindMapping(sourceType, targetType) as IUserMapping; + var inlineCtx = new InlineExpressionMappingBuilderContext(ctx, userMapping, mappingKey); var mapping = inlineCtx.BuildMapping(mappingKey, MappingBuildingOptions.KeepUserSymbol); if (mapping == null) return null; @@ -28,31 +38,17 @@ public static class QueryableMappingBuilder return new QueryableProjectionMapping(ctx.Source, ctx.Target, mapping); } - private static bool TryBuildMappingKey(MappingBuilderContext ctx, out TypeMappingKey mappingKey) + private static TypeMappingKey TryBuildMappingKey(MappingBuilderContext ctx, ITypeSymbol sourceType, ITypeSymbol targetType) { - mappingKey = default; - if (!ctx.Source.ImplementsGeneric(ctx.Types.Get(typeof(IQueryable<>)), out var sourceQueryable)) - return false; - - if (!ctx.Target.ImplementsGeneric(ctx.Types.Get(typeof(IQueryable<>)), out var targetQueryable)) - return false; - // if nullable reference types are disabled // and there was no explicit nullable annotation, // the non-nullable variant is used here. // Otherwise, this would lead to a select like source.Select(x => x == null ? throw ... : new ...) // which is not expected in this case. // see also https://github.com/riok/mapperly/issues/1196 - var sourceType = ctx.SymbolAccessor.NonNullableIfNullableReferenceTypesDisabled( - sourceQueryable.TypeArguments[0], - ctx.UserMapping?.SourceType - ); - var targetType = ctx.SymbolAccessor.NonNullableIfNullableReferenceTypesDisabled( - targetQueryable.TypeArguments[0], - ctx.UserMapping?.TargetType - ); - - mappingKey = new TypeMappingKey(sourceType, targetType); - return true; + sourceType = ctx.SymbolAccessor.NonNullableIfNullableReferenceTypesDisabled(sourceType, ctx.UserMapping?.SourceType); + targetType = ctx.SymbolAccessor.NonNullableIfNullableReferenceTypesDisabled(targetType, ctx.UserMapping?.TargetType); + + return new TypeMappingKey(sourceType, targetType); } } diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs index 805ea2fa81..adfba4e959 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs @@ -181,4 +181,38 @@ public Task RecordToClassDisabledNullableContext() return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); } + + [Fact] + public Task RecordToRecordMemberMappingDisabledNullableContext() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial System.Linq.IQueryable Map(System.Linq.IQueryable q); + + [MapProperty(nameof(A.Value), nameof(B.OtherValue)] + private partial B Map(A source); + """, + "public record A(string Value);", + "public record B(string OtherValue);" + ); + + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } + + [Fact] + public Task ClassToClassMemberMappingDisabledNullableContext() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial System.Linq.IQueryable Map(System.Linq.IQueryable q); + + [MapProperty(nameof(A.Value), nameof(B.OtherValue)] + private partial B Map(A source); + """, + "public class A { public string Value {get; set;} }", + "public class B { public string OtherValue {get; set;} }" + ); + + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } } diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs new file mode 100644 index 0000000000..8e47b99754 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs @@ -0,0 +1,31 @@ +//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? q) + { + if (q == null) + return default; +#nullable disable + return System.Linq.Queryable.Select( + q, + x => new global::B() + { + OtherValue = x.Value, + } + ); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B? Map(global::A? source) + { + if (source == null) + return default; + var target = new global::B(); + target.OtherValue = source.Value; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs new file mode 100644 index 0000000000..e278019f42 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs @@ -0,0 +1,24 @@ +//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? q) + { + if (q == null) + return default; +#nullable disable + return System.Linq.Queryable.Select(q, x => new global::B(x.Value)); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B? Map(global::A? source) + { + if (source == null) + return default; + var target = new global::B(source.Value); + return target; + } +} \ No newline at end of file