Using HotChocolate v15.1.8 with EF v9.0.7 and Mapster v7.4.0, I encounter a bug with [UseSorting] / [UseFiltering] LinQ conversions which appear invalid when applied to DDD ValueObject or EF complex property.
Executing this graphQL Query :
query Test {
legalEntity(where: { label: { eq: "TestSociety" } }) {
id
label
}
}
Gives me the following error :
"extensions": {
"message": "The LINQ expression 'DbSet<LegalEntity>()\r\n .Where(l => l.Label.Value == __p_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.",
"stackTrace": " at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)\r\n at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorExpression[TResult](Expression query)\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__DisplayClass11_0`1.<ExecuteCore>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.ExecuteCore[TResult](Expression query, Boolean async, CancellationToken cancellationToken)\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)"
}
CODE
To obtain this result, I use HotChocolate
//Query
[Authorize]
[QueryType]
public class LegalEntityQueries
{
[UseFiltering]
[UseSorting]
public IQueryable<LegalEntityGql> GetLegalEntities(
AppDbContext dbContext,
int? limit = null,
int? offset = null
)
{
return dbContext.LegalEntities.TakeIf(limit).SkipIf(offset).ProjectToType<LegalEntityGql>();
}
}
To avoid my whole entity to be public, I have a Gql DTO with equal names
//DTO
public record LegalEntityGql
{
public Guid Id { get; init; }
public string Label { get; init; } // As you can see, Label is here as a primitive string
}
To convert my entity to a Gql, I use Mapster v7.4.0
//Mapster Config
internal sealed class ValueObjectMapsterConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Label, string>().MapWith(l => l.Value);
}
}
Mapster and Hot Chocolate are configured in my program.cs with "AddGraphQL":
//Hot Chocolate config
public static class DependencyInjection
{
public static IServiceCollection AddGraphQL(this IServiceCollection services)
{
TypeAdapterConfig.GlobalSettings.Scan(typeof(ValueObjectMapsterConfig).Assembly);
services
.AddGraphQLServer()
.AddAuthorization()
.AddGraphQLTypes()
.AddProjections()
.AddFiltering()
.AddSorting();
MapsterConfiguration.RegisterMappings();
return services;
}
}
I'm following a DDD perspective, with Domain including aggregated roots and value object usings :
//Domain object
public class LegalEntity : BaseEntity
{
// 1. Properties
public Label Label { get; private set; }
// 2. Private constructor (used by factories)
private LegalEntity(Guid? id, Label labelt)
: base(id)
{
Label = label;
}
// 3. Factory methods
public static Result<LegalEntity> Create(Label label) =>
CreateFull(null, label);
public static Result<LegalEntity> CreateFull(
Guid? id,
Label label,
)
{
//To be reused immediatly, each legalEntity should be created using a new Guid
Guid legalEntityId = id ?? Guid.NewGuid();
return new LegalEntity(legalEntityId, label);
}
// 4. Muleability methods (behavior/modification)
public void UpdateLabel(Label label)
{
Label = label;
}
// ORM constructor
private LegalEntity() { }
}
My Label property is designed as a ValueObject, to preserve immutability inside the domain :
//ValueObject "Label"
public sealed class Label : ValueObject
{
public const int MaxLength = 50;
public string Value { get; }
private Label(string value)
{
Value = value;
}
public static Result<Label> Create(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return Result.Failure<Label>(DomainErrors.Label.Empty);
}
return new Label(value);
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Value;
}
// ORM constructor
private Label() { }
}
However, EF persist it as a single string in SQL, and convert it when reading it from db :
public static class AdminModelBuilderExtension
{
public static void ConfigureAdminModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<LegalEntity>(entity =>
{
entity
.Property(le => le.Label)
.HasConversion(le => le.Value, v => Label.Create(v).Value);
entity.HasIndex(le => le.Label).IsUnique();
});
}
}
Base of explanation
To read the GraphQL field Label, there is no problem using Mapster and the Label.Value conversion. But UseSorting/UseFiltering are automatically converting the (where: { label: { eq: "TestSociety" } }) expression to an invalid LinQ sequence. I've reproduced the problem by building 2 EF queries :
LegalEntity? test1 = await dbContext
.LegalEntities.OrderBy(le => le.Label)
.FirstOrDefaultAsync(CancellationToken.None);
LegalEntity? test2 = await dbContext
.LegalEntities.OrderBy(le => le.Label.Value)
.FirstOrDefaultAsync(CancellationToken.None);
In this example, test1 works successfully (and it's fine!), but test2 emits the same error :
The LINQ expression 'DbSet()\r\n .OrderBy(le => le.Label.Value)' could not be translated.
Thanks!