ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.19k stars 739 forks source link

Projection issues (overselect fields) with AutoMapper 11+ and EF Core #5724

Open Torellonik opened 1 year ago

Torellonik commented 1 year ago

Is there an existing issue for this?

Product

Hot Chocolate

Describe the bug

We experienced issues with GraphQL HotChocolate UseProjection attribute after Automapper upgrade to version 12.0.1 (last one working version was 10.1.1).

Here's our simplified context:

RentComponent and RentSection entity classes
public class RentResponse 
{
    public int Id { get; set; }

    public string Name { get; set; } = null!;

    public string? Description { get; set; }
}

public class RentSection
{
    public int Id { get; set; }

    public string Name { get; set; } = null!;

    public ICollection<RentComponent> Products { get; set; } = null!;
}
RentResponse and RentSectionResponse DTO classes
public class RentResponse 
{
    [IsProjected] // This forces projection on Id (it is always fetched so we can use it for filtering in field resolvers
    public int Id { get; set; }

    public string Name { get; set; } = null!;

    public string? Description { get; set; }

    public IEnumerable<RentSectionResponse> Sections { get; set; } = null!;
}

public class RentSectionResponse
{
    /// <summary>
    /// Id of the component
    /// </summary>
    public int Id { get; set; }

    public string Name { get; set; } = null!;

    public ICollection<RentResponse> Products { get; set; } = null!;
}
Mapping profile
CreateMap<RentComponent, RentResponse>()
            .ForAllMembers(o => o.ExplicitExpansion());

CreateMap<RentSection, RentSectionResponse>()
            .ForAllMembers(o => o.ExplicitExpansion());
Resolver
[UsePaging(IncludeTotalCount = true, DefaultPageSize = 10)]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<RentResponse> GetRents(
        AppDbContext db,
        [Service] IMapper mapper,
        IResolverContext context,
        [Service] ILogger<RentQueryResolver> logger
    )
    {
        logger.LogInformation("hit, GetRents!");
        return db.RentItems
            .ProjectTo<RentComponent, RentResponse>(context); 
    }

Making a following query:

rents(first: 1) {
  nodes { id, name, description }
}

The filter recognizes all selections, but generates a query to select only Id

[12:28:35 INF] Executed DbCommand (2ms) [Parameters=[@__ef_filter__p_0='True', @__p_0='2'], CommandType='Text', CommandTimeout='30']
SELECT r."Id"
FROM "RentItems" AS r
WHERE @__ef_filter__p_0 AND NOT (r."IsDeleted")
LIMIT @__p_0

And that is obviously causing errors in retrieving data since Name is not nullable in response DTO, but it's not retrieved.

We tried to downgrade to Automapper 10.1.1 that seems the last compatible version and i obtained following (right and expected) result:

SELECT r."Description", r."Id", r."Name"
FROM "RentItems" AS r
WHERE @__ef_filter__p_0 AND NOT (r."IsDeleted")
LIMIT @__p_0

We also noticed that there are some similar open issue. We tried to follow the steps written here and here but nothing worked.

Steps to reproduce

  1. Create a simple context based on entity / DTO classes.
  2. Create a mapping profile with Automapper 11+ using .ForAllMember(o => o.ExplicitExpansion())
  3. Create a resolver for that entity and add UseProjection attribute to it.
  4. Query for some properties of the entity

Relevant log output

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        9,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        8,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        7,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        6,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        5,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        4,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        3,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        2,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        1,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    },
    {
      "message": "Cannot return null for non-nullable field.",
      "locations": [
        {
          "line": 5,
          "column": 7
        }
      ],
      "path": [
        "rents",
        "nodes",
        0,
        "name"
      ],
      "extensions": {
        "code": "HC0018"
      }
    }
  ],
  "data": {
    "rents": {
      "nodes": null
    }
  }
}

Additional Context?

Notice that by removing ExplicitExpansion from mapping profiles, the data is retrieved due to the fact that a complete (without projection) SQL query is generated.

Version

12.5.2

Driedas commented 3 months ago

I too had this issue, after some searching I found the comment https://github.com/ChilliCream/graphql-platform/issues/4114#issuecomment-1518695940 by BlackDice051 and applied the code at the bottom, effectively replacing the HotChocolate AutoMapper integration. Hopefully this (or a similar fix) can be integrated into the offical HotChocolate.Data.AutoMapper package