diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 55874bdd17..e41b7216de 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -56,6 +56,59 @@ such mappings have several limitations: - Deep cloning is not applied ::: +## Null handling + +By default, Mapperly emits null-safe projections: nullable source members are guarded so mapping a +`null` value does not throw. When a projection is translated to SQL by a relational provider (such as +Entity Framework Core), these guards become `CASE WHEN` expressions that can prevent the database from +using indexes. Mapping a nullable source member to a non-nullable target member additionally reports +the `RMG089` diagnostic. + +If your projections are only ever executed by a relational provider (where the lambda is translated to +a query and never run in memory), you can skip this null handling with +`QueryableProjectionNullHandling.Ignore`: + + + + +```csharp +// highlight-start +[Mapper(QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore)] +// highlight-end +public static partial class CarMapper +{ + public static partial IQueryable ProjectToDto(this IQueryable q); +} +``` + + + + +```csharp +// with Ignore: source members are projected directly +q.Select(x => new CarDto() { OwnerName = x.Owner.Name }); + +// default (NullSafe): nullable members are guarded +q.Select(x => new CarDto() { OwnerName = x.Owner != null ? x.Owner.Name : default }); +``` + + + + +With `Ignore`, Mapperly projects source members directly, producing cleaner SQL and skipping the +related nullability diagnostics. The generated projection is then identical regardless of the nullable +reference type context. + +:::warning +`QueryableProjectionNullHandling.Ignore` assumes source members are non-`null`. Using it with an +in-memory `IQueryable` (e.g. LINQ to Objects) where a source value is actually `null` throws a +`NullReferenceException`. Only use it with providers that translate the projection to a query, such as +Entity Framework Core. +::: + +The default is `QueryableProjectionNullHandling.NullSafe`, which keeps the null-safe behavior described +above. + ## Property configurations To configure property mappings add partial mapping method definitions with attributes as needed. diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index f0e1190379..87342a2596 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -142,4 +142,10 @@ public class MapperAttribute : Attribute /// partial methods are discovered. /// public bool AutoUserMappings { get; set; } = true; + + /// + /// Controls how null values are handled in projection mappings. + /// Defaults to . + /// + public QueryableProjectionNullHandling QueryableProjectionNullHandling { get; set; } = QueryableProjectionNullHandling.NullSafe; } diff --git a/src/Riok.Mapperly.Abstractions/QueryableProjectionNullHandling.cs b/src/Riok.Mapperly.Abstractions/QueryableProjectionNullHandling.cs new file mode 100644 index 0000000000..c4913cd659 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/QueryableProjectionNullHandling.cs @@ -0,0 +1,19 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Controls how null values are handled in projection mappings. +/// +public enum QueryableProjectionNullHandling +{ + /// + /// Emits null-safe projections based on the nullable reference type annotations (default). + /// + NullSafe = 0, + + /// + /// Skips null handling in queryable projections, even when the source may be null. + /// Intended for relational providers (e.g. EF Core) where the projection is translated to SQL + /// and the lambda is never executed. Using this with in-memory queryables may cause a . + /// + Ignore = 1, +} diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 68893d366a..bc4d127ad9 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -135,4 +135,7 @@ public record MapperConfiguration /// Can be overwritten on specific enums via mapping method configurations. /// public EnumNamingStrategy? EnumNamingStrategy { get; init; } + + /// + public QueryableProjectionNullHandling? QueryableProjectionNullHandling { get; init; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 4d625f49f3..a509868356 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -27,6 +27,7 @@ public static MapperConfiguration Merge(MapperConfiguration highPriority, Mapper PreferParameterlessConstructors = highPriority.PreferParameterlessConstructors ?? lowPriority.PreferParameterlessConstructors, AutoUserMappings = highPriority.AutoUserMappings ?? lowPriority.AutoUserMappings, EnumNamingStrategy = highPriority.EnumNamingStrategy ?? lowPriority.EnumNamingStrategy, + QueryableProjectionNullHandling = highPriority.QueryableProjectionNullHandling ?? lowPriority.QueryableProjectionNullHandling, }; } @@ -104,6 +105,11 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.EnumNamingStrategy = mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy; + mapper.QueryableProjectionNullHandling = + mapperConfiguration.QueryableProjectionNullHandling + ?? defaultMapperConfiguration.QueryableProjectionNullHandling + ?? mapper.QueryableProjectionNullHandling; + return mapper; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index 1f6f950fe2..2e25e7e07b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -90,8 +91,11 @@ public static bool TryBuild( var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); + var ignoreProjectionNulls = IsIgnoredProjectionNullHandling(ctx.BuilderContext); + if ( memberMappingInfo.Configuration?.SuppressNullMismatchDiagnostic != true + && !ignoreProjectionNulls && memberSourceNullable && !memberTargetAcceptsNull && !(delegateSourceNullable && !delegateTargetNullable) @@ -136,6 +140,9 @@ public static bool TryBuild( return true; } + private static bool IsIgnoredProjectionNullHandling(MappingBuilderContext ctx) => + ctx.IsExpression && ctx.Configuration.Mapper.QueryableProjectionNullHandling == QueryableProjectionNullHandling.Ignore; + private static bool ValidateLoopMapping( IMembersBuilderContext ctx, INewInstanceMapping delegateMapping, @@ -173,8 +180,10 @@ private static NullMappedMemberSourceValue BuildInlineNullHandlingMapping( ITypeSymbol targetMemberType ) { + var ignoreNullHandling = IsIgnoredProjectionNullHandling(ctx.BuilderContext); + var nullFallback = NullFallbackValue.Default; - if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyReadNullable()) + if (!ignoreNullHandling && !delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyReadNullable()) { nullFallback = ctx.BuilderContext.GetNullFallbackValue(targetMemberType); } @@ -184,7 +193,8 @@ ITypeSymbol targetMemberType sourcePath.BuildGetter(ctx.BuilderContext), targetMemberType, nullFallback, - !ctx.BuilderContext.IsExpression + !ctx.BuilderContext.IsExpression, + ignoreNullHandling ); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs index da4dbe9e8e..8292c13e2d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs @@ -17,15 +17,25 @@ public class NullMappedMemberSourceValue( MemberPathGetter sourceGetter, ITypeSymbol targetType, NullFallbackValue nullFallback, - bool useNullConditionalAccess + bool useNullConditionalAccess, + bool ignoreNullHandling = false ) : ISourceValue { private readonly INewInstanceMapping _delegateMapping = delegateMapping; private readonly NullFallbackValue _nullFallback = nullFallback; private readonly MemberPathGetter _sourceGetter = sourceGetter; + private readonly bool _ignoreNullHandling = ignoreNullHandling; public ExpressionSyntax Build(TypeMappingBuildContext ctx) { + // if null handling should be ignored (e.g. queryable projections with + // QueryableProjectionNullHandling.Ignore), emit a straight member access. + if (_ignoreNullHandling) + { + ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source, addValuePropertyOnNullable: true)); + return _delegateMapping.Build(ctx); + } + // if the source is not nullable, return it directly. if (!_sourceGetter.MemberPath.IsAnyReadNullable()) { @@ -79,8 +89,9 @@ public override bool Equals(object? obj) var other = (NullMappedMemberSourceValue)obj; return _delegateMapping.Equals(other._delegateMapping) && _nullFallback == other._nullFallback - && _sourceGetter.Equals(other._sourceGetter); + && _sourceGetter.Equals(other._sourceGetter) + && _ignoreNullHandling == other._ignoreNullHandling; } - public override int GetHashCode() => HashCode.Combine(_delegateMapping, _nullFallback, _sourceGetter); + public override int GetHashCode() => HashCode.Combine(_delegateMapping, _nullFallback, _sourceGetter, _ignoreNullHandling); } diff --git a/src/Riok.Mapperly/Riok.Mapperly.targets b/src/Riok.Mapperly/Riok.Mapperly.targets index 6a1a0f4e24..80c5719eb0 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -23,5 +23,6 @@ + diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index 2491f7d24b..529eb579a4 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -140,6 +140,7 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.MemberVisibility IncludedMembers { get; set; } public bool PreferParameterlessConstructors { get; set; } public Riok.Mapperly.Abstractions.PropertyNameMappingStrategy PropertyNameMappingStrategy { get; set; } + public Riok.Mapperly.Abstractions.QueryableProjectionNullHandling QueryableProjectionNullHandling { get; set; } public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredEnumMappingStrategy { get; set; } public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredMappingStrategy { get; set; } public Riok.Mapperly.Abstractions.StackCloningStrategy StackCloningStrategy { get; set; } @@ -277,6 +278,11 @@ public enum PropertyNameMappingStrategy SnakeCase = 2, UpperSnakeCase = 3, } + public enum QueryableProjectionNullHandling + { + NullSafe = 0, + Ignore = 1, + } [System.Flags] public enum RequiredMappingStrategy { diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullHandlingTest.cs new file mode 100644 index 0000000000..9993522614 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullHandlingTest.cs @@ -0,0 +1,186 @@ +using Riok.Mapperly.Abstractions; + +namespace Riok.Mapperly.Tests.Mapping; + +public class QueryableProjectionNullHandlingTest +{ + [Fact] + public void IgnoreShouldSkipNullHandlingForNullableMember() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public C? Value { get; set; } }", + "class B { public D Value { get; set; } }", + "class C { public int Id { get; set; } }", + "class D { public int Id { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + #nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + Value = new global::D() + { + Id = x.Value.Id, + }, + } + ); + #nullable enable + """ + ); + } + + [Fact] + public Task IgnoreShouldNotRequireParameterlessCtorForNullableNavigation() + { + // https://github.com/riok/mapperly/issues/2350 + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public C? Nested { get; set; } }", + "class B { public D Nested { get; set; } }", + "class C { public int Id { get; set; } }", + "class D { public D(int id) { Id = id; } public int Id { get; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void IgnoreShouldNotReportNullableSourceDiagnostic() + { + // https://github.com/riok/mapperly/issues/1293 + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public string? Name { get; set; } }", + "class B { public string Name { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + #nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + Name = x.Name, + } + ); + #nullable enable + """ + ); + } + + [Fact] + public Task IgnoreDisabledNullableContextShouldMatchEnabled() + { + // https://github.com/riok/mapperly/issues/1293 — output must not depend on the nullable context + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public string? Name { get; set; } }", + "class B { public string Name { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } + + [Fact] + public void IgnoreShouldUnwrapNullableValueTypeMember() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public int? IntValue { get; set; } }", + "class B { public int IntValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + #nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + IntValue = x.IntValue.Value, + } + ); + #nullable enable + """ + ); + } + + [Fact] + public Task IgnoreShouldProjectDeeplyNestedNullablePathDirectly() + { + // relies on Mapperly's automatic flattening (Owner.Address.City => OwnerAddressCity), + // as MapProperty configuration is not supported directly on queryable projection mappings (RMG065). + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public C? Owner { get; set; } }", + "class B { public string OwnerAddressCity { get; set; } }", + "class C { public Address? Address { get; set; } }", + "class Address { public string City { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task IgnoreShouldProjectNullableCollectionMember() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + QueryableProjectionNullHandling = QueryableProjectionNullHandling.Ignore, + }, + "class A { public System.Collections.Generic.List? Items { get; set; } }", + "class B { public System.Collections.Generic.List Items { get; set; } }", + "class C { public int Id { get; set; } }", + "class D { public int Id { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } +} diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 5861f4b988..b95b60311a 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -107,6 +107,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.IncludedConstructors), Attribute(options.PreferParameterlessConstructors), Attribute(options.AutoUserMappings), + Attribute(options.QueryableProjectionNullHandling), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 6e166ff415..bbe1a10bb8 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -24,7 +24,8 @@ public record TestSourceBuilderOptions( MemberVisibility? IncludedConstructors = null, bool Static = false, bool PreferParameterlessConstructors = true, - bool AutoUserMappings = true + bool AutoUserMappings = true, + QueryableProjectionNullHandling? QueryableProjectionNullHandling = null ) { public const string DefaultMapperClassName = "Mapper"; diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreDisabledNullableContextShouldMatchEnabled#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreDisabledNullableContextShouldMatchEnabled#Mapper.g.verified.cs new file mode 100644 index 0000000000..ea06718d01 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreDisabledNullableContextShouldMatchEnabled#Mapper.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] + private partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? source) + { + if (source == null) + return default; +#nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + Name = x.Name, + } + ); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldNotRequireParameterlessCtorForNullableNavigation#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldNotRequireParameterlessCtorForNullableNavigation#Mapper.g.verified.cs new file mode 100644 index 0000000000..031eca90ca --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldNotRequireParameterlessCtorForNullableNavigation#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//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 global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + Nested = new global::D(x.Nested.Id), + } + ); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectDeeplyNestedNullablePathDirectly#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectDeeplyNestedNullablePathDirectly#Mapper.g.verified.cs new file mode 100644 index 0000000000..8dbfb7844a --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectDeeplyNestedNullablePathDirectly#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//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 global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + OwnerAddressCity = x.Owner.Address.City, + } + ); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectNullableCollectionMember#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectNullableCollectionMember#Mapper.g.verified.cs new file mode 100644 index 0000000000..72a1afd930 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullHandlingTest.IgnoreShouldProjectNullableCollectionMember#Mapper.g.verified.cs @@ -0,0 +1,27 @@ +//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 global::System.Linq.Queryable.Select( + source, + x => new global::B() + { + Items = global::System.Linq.Enumerable.ToList( + global::System.Linq.Enumerable.Select( + x.Items, + x1 => new global::D() + { + Id = x1.Id, + } + ) + ), + } + ); +#nullable enable + } +} \ No newline at end of file