I’m using Hot Chocolate to query peoples:
using Microsoft.EntityFrameworkCore;
namespace SageApi.Model.Sage;
public partial class People
{
public string Id { get; set; } = null!;
public int? Age { get; set; }
public string? Name { get; set; }
public static void ConfigureModelBuilder(ModelBuilder modelBuilder)
{
modelBuilder.Entity<People>(entity =>
{
entity.HasIndex(e => e.Id, "UKA_PEOPLE_Id").IsUnique();
entity.Property(e => e.Age).HasColumnName("Age");
entity.Property(e => e.Name).HasColumnName("Name");
});
}
}
What I want to do ?
I want the people who use my API to be able to query the peoples who are over 21 years old without having to specify the age 21 (this is a simplify example of what I want to do, to keep it simple).
Something like:
{
peoples(where: { aboveAge: { eq: true } }) {
items {
id
age
name
}
}
}
What I tried
So I added a field AboveAge
in People
like so (this way I can query this field even if this field doesn’t exists in the bdd):
using System.ComponentModel.DataAnnotations.Schema;
// ...
public string? Name { get; set; }
[IsProjected(false)] [NotMapped] public virtual bool? AboveAge { get; set; }
public static void ConfigureModelBuilder(ModelBuilder modelBuilder)
// ...
In my Query.cs
, I use UseOffsetPaging
, UseProjection
, UseFiltering
, UseSorting
and my custom UseAboveAgeFilter
to modify the query before UseFiltering
is called
public class Query
{
[UseOffsetPaging(MaxPageSize = 100, IncludeTotalCount = true, DefaultPageSize = 20)]
[UseProjection]
[UseFiltering]
[UseSorting]
[UseAboveAgeFilter]
public IEnumerable<People> GetPeoples([Service] AppDbContext context) =>
context.Peoples;
}
In my UseAboveAgeFilter.cs
:
using System.Reflection;
using System.Runtime.CompilerServices;
using HotChocolate.Types.Descriptors;
using SageApi.Model.Sage;
namespace SageApi.Services.GraphQl.Middleware;
public class UseAboveAgeFilterAttribute : ObjectFieldDescriptorAttribute
{
public UseAboveAgeFilterAttribute([CallerLineNumber] int order = 0)
{
Order = order;
}
protected override void OnConfigure(
IDescriptorContext context,
IObjectFieldDescriptor descriptor,
MemberInfo member
)
{
descriptor.Use(next => async middlewareContext =>
{
// can I modify the middlewareContext in a way that I remove aboveAge param and replace it
// with age >= 21, this way the UseFiltering Middleware will work as intended when it will
// be called after ?
await next(middlewareContext);
object? result = middlewareContext.Result;
if (result is InternalDbSet<People> dbSet)
{
// or can I do it here ?
}
});
}
}
I want to modify the middlewareContext by removing the aboveAge = true
condition and replace it with age >= 21
condition but I can’t find a way to it. If I can do that in my Middleware UseAboveAgeFilter
which is call before all the Hot Chocolate Middleware it should work.
When I try to query I got error (which is normal, as I didn’t modify the middlewareContext to remove aboveAge = true
condition and replace it with age >= 21
):
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [
{
"line": 2,
"column": 5
}
],
"path": [
"peoples"
],
"extensions": {
"message": "The LINQ expression 'DbSet<People>()rn .Where(f => f.AboveAge == __p_0)' could not be translated. Additional information: Translation of member 'AboveAge' on entity type 'People' failed. This commonly occurs when the specified member is unmapped. 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)rn at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)rn at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)rn at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)rn at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)rn at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()rn at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)rn at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)rn at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)rn at System.Linq.Queryable.Count[TSource](IQueryable`1 source)rn at HotChocolate.Types.Pagination.QueryableOffsetPagingHandler`1.<>c__DisplayClass3_0.<ResolveAsync>b__0()rn at System.Threading.Tasks.Task`1.InnerInvoke()rn at System.Threading.Tasks.Task.<>c.<.cctor>b__281_0(Object obj)rn at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)rn--- End of stack trace from previous location ---rn at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)rn at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)rn--- End of stack trace from previous location ---rn at HotChocolate.Types.Pagination.QueryableOffsetPagingHandler`1.ResolveAsync(IResolverContext context, IQueryable`1 source, OffsetPagingArguments arguments, CancellationToken cancellationToken)rn at HotChocolate.Types.Pagination.OffsetPagingHandler.HotChocolate.Types.Pagination.IPagingHandler.SliceAsync(IResolverContext context, Object source)rn at HotChocolate.Types.Pagination.PagingMiddleware.InvokeAsync(IMiddlewareContext context)rn at HotChocolate.Utilities.MiddlewareCompiler`1.ExpressionHelper.AwaitTaskHelper(Task task)rn at SageApi.Services.GraphQl.Middleware.UseFDocenteteMappingAttribute.<>c__DisplayClass1_0.<<OnConfigure>b__1>d.MoveNext() in C:\Users\sagealexandrehome\Documents\SageApi\SageApi\Services\GraphQl\Middleware\UseFDocenteteMappingAttribute.cs:line 26rn--- End of stack trace from previous location ---rn at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)rn at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"
}
}
],
"data": {
"peoples": null
}
}
Question
Am I on the right path to solve this problem ?
If yes, how can I continue ?
If no, what can I try ?
1
You need to go through the middlewareContext.Selection.Arguments
and replace the argument you need by another:
using System.Reflection;
using System.Runtime.CompilerServices;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types.Descriptors;
namespace SageApi.Services.GraphQl.Middleware;
public class UseAboveAgeFilterAttribute : ObjectFieldDescriptorAttribute
{
public UseAboveAgeFilterAttribute([CallerLineNumber] int order = 0)
{
Order = order;
}
protected override void OnConfigure(
IDescriptorContext context,
IObjectFieldDescriptor descriptor,
MemberInfo member
)
{
descriptor.Use(next => async middlewareContext =>
{
await next(middlewareContext);
var arguments = middlewareContext.Selection.Arguments;
foreach (var argument in arguments)
{
if (argument.Name == "where")
{
var objectFieldNodes = (List<ObjectFieldNode>)argument.ValueLiteral?.Value;
foreach (var objectFieldNode in objectFieldNodes.ToList())
{
if (objectFieldNode.Name.Value == "aboveAge")
{
objectFieldNodes.Remove(objectFieldNode);// remove above age
// add age >= 21
var newObjectFieldNode = new ObjectFieldNode(
"age",
new ObjectValueNode(new List<ObjectFieldNode>
{
new("gte", new IntValueNode(21))
})
);
objectFieldNodes.Add(newObjectFieldNode);
}
}
middlewareContext.ReplaceArgument("where", new ArgumentValue(
argument,
argument.Kind ?? ValueKind.Object,
argument.IsFullyCoerced,
argument.IsDefaultValue,
argument.Value,
new ObjectValueNode(objectFieldNodes)
));
}
}
});
}
}
```
8