Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected IQueryable mapping when project's Nullable flag is set to "disable" #1293

Open
mpickers opened this issue May 22, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@mpickers
Copy link

mpickers commented May 22, 2024

Describe the bug
When you have a project setup as disable, 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"
@mpickers mpickers added the bug Something isn't working label May 22, 2024
@mpickers
Copy link
Author

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?

@latonz
Copy link
Contributor

latonz commented May 22, 2024

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?

@mpickers
Copy link
Author

@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'.

@fil-at-werma
Copy link

fil-at-werma commented Nov 26, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants