0

Is there a way to use projections if I don't want my services to return IQueryable?

I am thinking of something like, my resolver could map input to an Expression and then call the service class, then query is built and executed based on the provided expression, converted to some read only collection and returned to the user.

Something like the following:

 public async Task<IReadOnlyList<TestPayload>> GetAllTests(
                                             [Service] ITestService, 
                                             CancellationToken cancellationToken, 
                                             object[] requestedFields) => 
            await service.GetAllAsync(requestedFields, cancellationToken);

Then in my service:

var expression = BuildExpression(requestedFields);

var output = testRepository.Queryable()
                                 .Select(expression)
                                    .AsNoTracking();
return output.ToListAsync();

Can you point me in some direction regarding this?


EDIT:

As per @Pascal's answer I added HotChocolate.Data package and invoked Project(context) extension method on my IQueryable.

Right now I have an issue with my payload(model) classes, but I'm not quite sure why.

The following example will return all columns and will not apply projection.

       [UseProjection]
       public async Task<IReadOnlyList<TestPayload>> TestGetAll([Service] ITestService testService, IResolverContext context, CancellationToken cancellationToken) 
        {
            return await testService.GetAllAsync(context, cancellationToken);
        }

This one will work, and will project to the query and return only what's requested. Notice that the only thing that's different is the return type (IReadOnly<Test> instead of IReadOnly<TestPayload>).

       [UseProjection]
       public async Task<IReadOnlyList<Test>> TestGetAll([Service] ITestService testService, IResolverContext context, CancellationToken cancellationToken) 
        {
            return await testService.GetAllAsync(context, cancellationToken);
        }

Inside the service:

var tests = testRepository.Queryable()
                         .Project<Test>(context)
                        .AsNoTracking();

I guess it is because of the resolver that is now working with the dto TestPayload type and then inside the service trying to project to the IQueryable of entity(Test), but I'm not sure how to overcome it.

Bennett McElwee
  • 24,740
  • 6
  • 54
  • 63
Temp034
  • 141
  • 11

2 Answers2

2

Yes there is, HotChocolate.Data provides extension methods on top of IQueryable

 public async Task<IReadOnlyList<TestPayload>> GetAllTests(
    [Service] ITestService, 
    CancellationToken cancellationToken, 
    IResolverContext context) => 
    await service.GetAllAsync(context, cancellationToken);
var output = testRepository.Queryable()
     .Project(context)
     .AsNoTracking();
return output.ToListAsync();
Pascal Senn
  • 1,712
  • 11
  • 20
  • 1
    Nice, but for some strange reason it does not work when I return dto from my service, and when return type is entity it's just fine. I inspected the query and noticed that .Project(context) is ignored in that case, so I get all my columns back. Maybe I missed something obvious, I'm looking into it. So in the example above, when return type is TestPayload, it won't work, but if I switch it to Test (which is my entity class) it's good. – Temp034 Jan 10 '22 at 14:23
  • I have added this to the question edit. What is the best way to keep DTO's as I don't want my services to return entities? I know DTO's may be sort of an anti-pattern, but I may be using my services for things other than GraphQL. – Temp034 Jan 10 '22 at 17:10
  • can you provide a link to read about this please ? – CSharp-n Apr 15 '22 at 07:18
0

There is also antother way I found, that is working with the EntityFramework:

Here my Service Class

public async Task<IEnumerable<TechData>> GetAllTechData(IResolverContext resolverContext, CancellationToken cancellationToken = default)
    {
        return _context.TechData.AsQueryable().Project(resolverContext);
    }

And here the query

    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public async Task<IEnumerable<TechData>> GetTechData([Service] TechDataRepository techDataRepository, IResolverContext rcontext, CancellationToken cancellationToken)
        => await techDataRepository.GetAllTechData(rcontext, cancellationToken);
  • You can also use Where-Statements and so on behind it. For example: return _context.TechData.AsQueryable().Project(resolverContext).Where(t => t.MaterialId == matNr).ToList(); – denis-trtnr Jun 18 '23 at 21:43