Вопрос или проблема
У меня есть резолвер, который возвращает 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 не может обработать.
Почему это происходит?
-
Разница в подходах:
- Использование
ProjectTo
предназначено для автоматического создания проекций, основываясь на конфигурациях маппинга. Однако, когда у вас есть сложные структуры данных, такие как коллекции, EF Core может сбиться с толку и не суметь правильно сформировать SQL-запрос. - В случае одноуровневого маппинга, как в вашем примере с
IQueryable
, EF Core может генерировать соответствующий SQL-запрос без проблем.
- Использование
-
Трудности с коллекциями: Связь между сущностями может приводить к сложности трансляции, особенно если вы пытаетесь создать новые экземпляры объектов в
Select
внутриProjectTo
.
Решения проблемы
-
Использование
Select
вместоProjectTo
: Как вы уже обнаружили, использованиеSelect
для создания проекции объектов вручную работает:var graphqlModelQueryable = dataModelQueryable.Select(x => new GraphQL.Models.Patient() { // Маппинг полей });
Это решение, хотя и вручную написанное, более предсказуемое и позволяет контролировать, как EF Core формирует запросы.
-
Использование расширений для AutoMapper: Чтобы избежать написания громоздкого кода маппинга вручную, вы можете использовать расширения для
AutoMapper
, такие какAutoMapper.Extensions.Microsoft.DependencyInjection
, если они доступны для работы с EF Core:var graphqlModelQueryable = dataModelQueryable.ProjectTo<GraphQL.Models.Patient>(Mapper.ConfigurationProvider);
Иногда, конфигурация маппинга может включать непосредственно коллекции, что позволяет правильно направлять AutoMapper при преобразовании.
-
Создание ViewModel: Создание отдельной модели (ViewModel) для обработки запроса может упростить маппинг. Вы можете иметь более простые модели, которые не содержат коллекции, и обрабатывать их отдельно перед созданием окончательной модели.
-
Упрощение маппинга: Если коллекции являются сложными для автоматизации, рассмотрите вариант создания отдельного маппинга для коллекций.
Заключение
Проблема с трансляцией LINQ-запросов при использовании AutoMapper
и Hot Chocolate
в коде, где есть коллекции, может быть решена с помощью переосмысления подхода к маппингу. Использование Select
вместо ProjectTo
, создание отдельных ViewModel и использование расширенных возможностей AutoMapper может значительно упростить вашу работу и сделать код более понятным.
Такой подход не только решит проблему, но и обеспечит вам гибкость в дальнейшей разработке и внедрении новых функциональных возможностей.
Если у вас будут дополнительные вопросы или требуется помощь в конкретных сценариях, не стесняйтесь обращаться за поддержкой.