горячий шоколад и автоматическое сопоставление не могли быть переведены, когда модель содержит коллекцию

Вопрос или проблема

У меня есть резолвер, который возвращает IQueryable из Entity Framework Core. Когда я возвращаю сущность EF Core напрямую, все работает хорошо. Однако, когда я пытаюсь сопоставить сущность EF Core с моей моделью GraphQL, запрос не выполняется должным образом, особенно когда модель содержит коллекцию.

Когда я делаю этот запрос с помощью ProjectTo<Graphql.Models.Patient>, он не выполняется, и я получаю ответ:

{
  "errors": [
    {
      "message": "Неожиданная ошибка выполнения",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "patients"
      ],
      "extensions": {
        "message": "Выражение LINQ 'p2 => new Session{ Token = p2.Token }\r\n' не может быть переведено. Либо перепишите запрос в форме, которая может быть переведена, либо переключитесь на клиентскую оценку явно, вставив вызов 'AsEnumerable', 'AsAsyncEnumerable', 'ToList' или 'ToListAsync'. Обратитесь к https://go.microsoft.com/fwlink/?linkid=2101038 за дополнительной информацией.",
        "stackTrace": "   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitLambda[T](Expression`1 lambdaExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)\r\n   at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateProjection(Expression expression, Boolean applyDefaultTypeMapping)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)\r\n   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitConditional(ConditionalExpression conditionalExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)\r\n   at System.Linq.Expressions.ExpressionVisitor.VisitMemberBinding(MemberBinding node)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)\r\n   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)\r\n   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)\r\n   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)\r\n   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)\r\n   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)\r\n   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()\r\n   at HotChocolate.DefaultAsyncEnumerableExecutable`1.ToListAsync(CancellationToken cancellationToken)\r\n   at HotChocolate.Execution.ListPostProcessor`1.ToCompletionResultAsync(Object result, CancellationToken cancellationToken)\r\n   at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)\r\n   at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"
      }
    }
  ],
  "data": null
}

Когда я запускаю это, используя IQueryable напрямую, я получаю ожидаемый результат:

{
  "data": {
    "patients": [
      {
        "user": {
          "address": {
            "street": "Elm St"
          },
          "sessions": [
            {
              "token": "token6"
            }
          ]
        }
      },
      {
        "user": {
          "address": {
            "street": "Pine Rd"
          },
          "sessions": [
            {
              "token": "token9"
            }
          ]
        }
      }
    ]
  }
}

Если я убираю список из моих моделей GraphQL (и также не запрашиваю его), все работает как ожидается.

Запрос:

query {
  patients {
    user {
      address {
        street
      }
      sessions {
        token
      }
    }
  }
}

Метод PatientRepository

        public IQueryable<Data.Table.Patient> GetPatientsQueryable()
        {
            return Context.Patients.AsNoTracking().AsQueryable();
        }

Примеры резолверов

        [UseProjection]        
        public async Task<IQueryable<Patient>> GetPatients([Service] IPatientRepository repository, [Service] IMapper Mapper)
        {
            //НЕ РАБОТАЕТ
            var dataModelQueryable = repository.GetPatientsQueryable();
            var graphqlModelQueryable = Mapper.ProjectTo<Patient>(dataModelQueryable);

            return graphqlModelQueryable;
        }

        [UseProjection]
        public async Task<IQueryable<Data.Tables.Patient>> GetPatients([Service] IPatientRepository repository, [Service] IMapper Mapper)
        {
            //РАБОТАЕТ
            var dataModelQueryable = repository.GetPatientsQueryable();
            return dataModelQueryable;
        }

Модели GraphQL

namespace Api.GraphQL.Models
{
    public class Patient
    {
        public User User { get; set; }

        public BloodType BloodType { get; set; }

        public int Height { get; set; }

        public decimal Weight { get; set; }
    }
}

namespace Api.GraphQL.Models
{
    public class User
    {
        public int Bsn { get; set; }

        public string FirstName { get; set; }
        public string MiddleName { get; set; } = string.Empty;
        public string LastName { get; set; }

        public DateOnly DateOfBirth { get; set; }

        //УДАЛЯЯ ЭТОТ СПИСОК, ОБЕ МЕТОДЫ РАБОТАЮТ
        public List<Session> Sessions { get; set; }
        public Address Address { get; set; }
    }
}

namespace Api.GraphQL.Models
{
    public class Session
    {
        public string Token { get; set; }
        public DateTime ExpirationDate { get; set; }
    }
}

Таблицы EF Core

namespace Data.Tables
{
    public class Patient
    {
        [Key, ForeignKey(nameof(User)), DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int UserBsn { get; set; }
        public virtual User User { get; set; }

        public BloodType BloodType { get; set; }

        public required int Height { get; set; }

        [Column(TypeName = "decimal(5,2)")]
        public required decimal Weight { get; set; }
    }
}

namespace Data.Tables
{
    public class User
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Bsn { get; set; }

        [MaxLength(255)]
        public required string? FirstName { get; set; }

        [MaxLength(255)]
        public string MiddleName { get; set; } = string.Empty;

        [MaxLength(255)]
        public required string LastName { get; set; }

        public DateOnly DateOfBirth { get; set; }

        public ICollection<Session> Sessions { get; set; } 
    }
}

namespace Data.Tables
{
    public class Session
    {
        [Key]
        public int Id { get; set; }

        [ForeignKey(nameof(User))]
        public int UserBsn { get; set; }
        public virtual User User { get; set; }

        [MaxLength(128)]
        public required string Token { get; set; }

        public required DateTime ExpirationDate { get; set; }
    }
}
using AutoMapper;

namespace Api
{
    public class MappingProfile : Profile
    {
        /// <summary>
        /// Профиль сопоставления для AutoMapper (таблицы данных в модели graphql)
        /// </summary>
        public MappingProfile()
        {
            CreateMap<Data.Tables.Patient, GraphQL.Models.Patient>();
            CreateMap<GraphQL.Models.Patient, Data.Tables.Patient>();

            CreateMap<Data.Tables.User, GraphQL.Models.User>();
            CreateMap<GraphQL.Models.User, Data.Tables.User>();

            CreateMap<Data.Tables.Session, GraphQL.Models.Session>();
            CreateMap<GraphQL.Models.Session, Data.Tables.Session>();

            CreateMap<Data.Tables.Address, GraphQL.Models.Address>();
            CreateMap<GraphQL.Models.Address, Data.Tables.Address>();
        }
    }
}

Рабочий ответ:

        public async Task<IQueryable<Patient>> GetPatients([Service] IPatientRepository repository, [Service] IMapper Mapper)
        {

            var dataModelQueryable = repository.GetPatientsQueryable();
            var graphqlModelQueryable = dataModelQueryable.Select(x => new GraphQL.Models.Patient()
            {
                BloodType = x.BloodType,
                Height = x.Height,
                User = new GraphQL.Models.User()
                {
                    Bsn = x.User.Bsn,
                    Address = new GraphQL.Models.Address()
                    {
                        City = x.User.Address.City,
                        CountryOfResidence = x.User.Address.CountryOfResidence,
                        HouseNumber = x.User.Address.HouseNumber,
                        Id = x.User.Address.Id,
                        PostalCode = x.User.Address.PostalCode,
                        Province = x.User.Address.PostalCode,
                        Street = x.User.Address.Street,
                    },
                    DateOfBirth = x.User.DateOfBirth,
                    FirstName = x.User.FirstName,
                    MiddleName = x.User.MiddleName,
                    LastName = x.User.LastName,
                    Sessions = x.User.Sessions.Select(session => new GraphQL.Models.Session()
                    {
                        ExpirationDate = session.ExpirationDate,
                        Token = session.Token,
                    }).ToList()
                },
                Weight = x.Weight
            });

            return graphqlModelQueryable;
        }

Переключение на .Select работает, но я предпочел бы продолжать использовать AutoMapper (или аналогичный инструмент), чтобы избежать написания этих больших методов преобразования. Есть какие-либо советы по этому поводу?

Ответ или решение

Решение проблемы с AutoMapper и Hot Chocolate при использовании коллекций в моделях GraphQL

Введение

Ситуация, с которой вы столкнулись, связана с трудностями в трансляции LINQ-запросов при работе с AutoMapper и Hot Chocolate, когда ваша модель содержит коллекцию. В данном ответе я рассмотрю основные причины возникновения ошибки и предложу возможные решения.

Описание проблемы

Когда вы пытаетесь использовать AutoMapper с коллекцией в вашей модели GraphQL, возникла ошибка:

The LINQ expression 'p2 => new Session{ Token = p2.Token }' could not be translated.

Это указывает на то, что выражение не может быть переведено в SQL-запрос, возможно, потому что оно использует операции, которые EF Core не может обработать.

Почему это происходит?

  1. Разница в подходах:

    • Использование ProjectTo предназначено для автоматического создания проекций, основываясь на конфигурациях маппинга. Однако, когда у вас есть сложные структуры данных, такие как коллекции, EF Core может сбиться с толку и не суметь правильно сформировать SQL-запрос.
    • В случае одноуровневого маппинга, как в вашем примере с IQueryable, EF Core может генерировать соответствующий SQL-запрос без проблем.
  2. Трудности с коллекциями: Связь между сущностями может приводить к сложности трансляции, особенно если вы пытаетесь создать новые экземпляры объектов в Select внутри ProjectTo.

Решения проблемы

  1. Использование Select вместо ProjectTo: Как вы уже обнаружили, использование Select для создания проекции объектов вручную работает:

    var graphqlModelQueryable = dataModelQueryable.Select(x => new GraphQL.Models.Patient()
    {
       // Маппинг полей
    });

    Это решение, хотя и вручную написанное, более предсказуемое и позволяет контролировать, как EF Core формирует запросы.

  2. Использование расширений для AutoMapper: Чтобы избежать написания громоздкого кода маппинга вручную, вы можете использовать расширения для AutoMapper, такие как AutoMapper.Extensions.Microsoft.DependencyInjection, если они доступны для работы с EF Core:

    var graphqlModelQueryable = dataModelQueryable.ProjectTo<GraphQL.Models.Patient>(Mapper.ConfigurationProvider);

    Иногда, конфигурация маппинга может включать непосредственно коллекции, что позволяет правильно направлять AutoMapper при преобразовании.

  3. Создание ViewModel: Создание отдельной модели (ViewModel) для обработки запроса может упростить маппинг. Вы можете иметь более простые модели, которые не содержат коллекции, и обрабатывать их отдельно перед созданием окончательной модели.

  4. Упрощение маппинга: Если коллекции являются сложными для автоматизации, рассмотрите вариант создания отдельного маппинга для коллекций.

Заключение

Проблема с трансляцией LINQ-запросов при использовании AutoMapper и Hot Chocolate в коде, где есть коллекции, может быть решена с помощью переосмысления подхода к маппингу. Использование Select вместо ProjectTo, создание отдельных ViewModel и использование расширенных возможностей AutoMapper может значительно упростить вашу работу и сделать код более понятным.

Такой подход не только решит проблему, но и обеспечит вам гибкость в дальнейшей разработке и внедрении новых функциональных возможностей.

Если у вас будут дополнительные вопросы или требуется помощь в конкретных сценариях, не стесняйтесь обращаться за поддержкой.

Оцените материал
Добавить комментарий

Капча загружается...