Unexpected IQueryable mapping when project's Nullable flag is set to "disable"
Describe the bug When you have a project setup as <Nullable>disable</Nullable>, the generated mapping for IQueryable sets the IQueryable code block with #nullable disable but the generated code in the block checks for nulls rather than just doing the straight mapping.
I would expect that for the IQueryable mappings setting Nullable to disable or enable would result in the same IQueryable mapping.
This null checking also results in some incorrect EF Core generated code when you have a child entity and mapping to a DTO.
Declaration code
[Mapper]
public static partial class CarrierServiceMapping
{
[MapperRequiredMapping(RequiredMappingStrategy.Target)]
[MapProperty([nameof(CarrierService.Carrier), nameof(Carrier.Name)], [nameof(CarrierServiceDto.CarrierName)])]
public static partial CarrierServiceDto ToCarrierServiceDto(this CarrierService service);
[MapperRequiredMapping(RequiredMappingStrategy.Target)]
public static partial CarrierServiceCountryInclusionDto CountryExclusionDto(
this CarrierServiceCountryInclusion countryInclusion);
public static partial IQueryable<CarrierServiceDto> ProjectToCarrierServiceDto(this IQueryable<CarrierService> q);
}
public class CarrierService
{
public int Id { get; set; }
public int CarrierId { get; set; }
public virtual Carrier Carrier { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<CarrierServiceCountryInclusion> CountryInclusions { get; set; }
}
public class Carrier
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CarrierServiceCountryInclusion
{
public int CarrierServiceId { get; set; }
public string CountryCode { get; set; }
public virtual CarrierService CarrierService { get; set; }
}
public class CarrierServiceDto
{
public int Id { get; set; }
public string Name { get; set; }
public string CarrierName { get; set; }
public string Description { get; set; }
public List<CarrierServiceCountryInclusionDto> CountryInclusions { get; set; }
}
public class CarrierServiceCountryInclusionDto
{
public int CarrierServiceId { get; set; }
public string CountryCode { get; set; }
}
Actual relevant generated code
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
public static partial global::System.Linq.IQueryable<global::MapperlyDebug.CarrierServiceDto?>? ProjectToCarrierServiceDto(this global::System.Linq.IQueryable<global::MapperlyDebug.Entities.CarrierService?>? q)
{
if (q == null)
return default;
#nullable disable
return System.Linq.Queryable.Select(q, x => x == null ? default : new global::MapperlyDebug.CarrierServiceDto()
{
Id = x.Id,
Name = x.Name,
CarrierName = x.Carrier != null && x.Carrier.Name != null ? x.Carrier.Name : default,
Description = x.Description,
CountryInclusions = x.CountryInclusions != null ? global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.CountryInclusions, x1 => x1 == null ? default : new global::MapperlyDebug.CarrierServiceCountryInclusionDto()
{
CarrierServiceId = x1.CarrierServiceId,
CountryCode = x1.CountryCode,
})) : default,
});
#nullable enable
}
Expected relevant generated code
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
public static partial global::System.Linq.IQueryable<global::MapperlyDebug.CarrierServiceDto?>? ProjectToCarrierServiceDto(this global::System.Linq.IQueryable<global::MapperlyDebug.Entities.CarrierService?>? q)
{
#nullable disable
return System.Linq.Queryable.Select(q, x => new global::MapperlyDebug.CarrierServiceDto()
{
Id = x.Id,
Name = x.Name,
CarrierName = x.Carrier.Name,
Description = x.Description,
CountryInclusions = global::System.Linq.Enumerable.ToList(global::System.Linq.Enumerable.Select(x.CountryInclusions, x1 => new global::MapperlyDebug.CarrierServiceCountryInclusionDto()
{
CarrierServiceId = x1.CarrierServiceId,
CountryCode = x1.CountryCode,
})),
});
#nullable enable
}
Environment (please complete the following information):
- Mapperly Version: 3.5.1 or 3.6.0 preview
- Nullable reference types: disable
- .NET Version: 8.0.101
- Target Framework: .net8.0
- Compiler Version: 4.8.0-7.23572.1 (7b75981c)'.
- C# Language Version: 12.0
- IDE: Visual Studio 17.8.6
- OS: Windows 10
Additional context The incorrect EF Core generated SQL when the Nullable flag is set to 'disable' is as follows (in particular notice the multiple LEFT JOIN lines):
SELECT 0, "c"."Id", "c"."Name", CASE
WHEN "c0"."Name" IS NOT NULL THEN "c0"."Name"
ELSE NULL
END, "c"."Description", "c0"."Id", "c1"."CarrierServiceId", "c1"."CountryCode", 0, "c2"."CarrierServiceId", "c2"."CountryCode"
FROM "CarrierServices" AS "c"
INNER JOIN "Carriers" AS "c0" ON "c"."CarrierId" = "c0"."Id"
LEFT JOIN "CarrierServiceCountryInclusions" AS "c1" ON "c"."Id" = "c1"."CarrierServiceId"
LEFT JOIN "CarrierServiceCountryInclusions" AS "c2" ON "c"."Id" = "c2"."CarrierServiceId"
ORDER BY "c"."Id", "c0"."Id", "c1"."CarrierServiceId", "c1"."CountryCode", "c2"."CarrierServiceId"
The correct EF Core generated SQL when the Nullable flag is set to 'enable' - and what I'd also expect if the flag was set to 'disable' is:
SELECT "c"."Id", "c"."Name", "c0"."Name", "c"."Description", "c0"."Id", "c1"."CarrierServiceId", "c1"."CountryCode"
FROM "CarrierServices" AS "c"
INNER JOIN "Carriers" AS "c0" ON "c"."CarrierId" = "c0"."Id"
LEFT JOIN "CarrierServiceCountryInclusions" AS "c1" ON "c"."Id" = "c1"."CarrierServiceId"
ORDER BY "c"."Id", "c0"."Id", "c1"."CarrierServiceId"
In another project I've also experienced the null checking output even when the Nullable property is set to 'enable' on the main project.
As it was a larger project, it appears that if there is a referenced project that doesn't have the Nullable property set at all, Mapperly seems to behave as if the Nullable property is set to disable.
Possibly a separate issue?
Thanks for reporting. Regarding your second comment: it depends on the ef core entity classes respectively their tproperties. Are they in a nullable disabled context?
@latonz happy to help.
As for multiple projects and nullable issue - ok, that would explain it then. The solution has been migrated from earlier .net so the Nullable option hasn't been set on all projects - and hence defaults to false - so the entities project would have been seen as Nullable 'false'.
Not sure if this is the same issue but I just stumbled over this:
public class MyEntity
{
public Guid MyChildId { get; set; }
public MyChildEntity Child { get; set; }
}
public class MyChildEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class MyDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
[Mapper]
[UseStaticMapper(typeof(UserRoleMapper))]
public static partial class TestProjectionMapperly
{
public static partial IQueryable<MyDto> ProjectToDto(this IQueryable<MyEntity> source);
[MapProperty(nameof(MyEntity.MyChildId), nameof(MyDto.Id))]
[MapProperty(nameof(MyEntity.Child.Name), nameof(MyDto.Name))]
[UserMapping]
private static partial MyDto MapToDto(this MyEntity source);
}
In a .Net 7 Project without explicit nullable switch (which I believe resolves to disabled?) generates
// <auto-generated />
#nullable enable
namespace Werma.WeAssist.UserManagement.Application.Mappers
{
public static partial class TestProjectionMapperly
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.1.0.0")]
public static partial global::System.Linq.IQueryable<global::MyNameSpace.Mappers.MyDto?>? ProjectToDto(this global::System.Linq.IQueryable<global::MyNameSpace.Mappers.MyEntity?>? source)
{
if (source == null)
return default;
#nullable disable
return System.Linq.Queryable.Select(
source,
x => new global::MyNameSpace.Mappers.MyDto()
);
#nullable enable
}
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.1.0.0")]
private static partial global::MyNameSpace.Mappers.MyDto? MapToDto(this global::MyNameSpace.Mappers.MyEntity? source)
{
if (source == null)
return default;
var target = new global::MyNameSpace.Mappers.MyDto();
target.Id = source.MyChildId;
target.Name = source.Child?.Name;
return target;
}
}
}
Adding #nullable enable fixes the projection:
// ...
#nullable disable
return System.Linq.Queryable.Select(
source,
x => new global::MyNameSpace.Mappers.MyDto()
{
Id = x.MyChildId,
Name = x.Child.Name,
}
);
#nullable enable
Mapperly 4.1.0