Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't wrap the Query calls in another method. #69

Closed
lukemurray opened this issue Aug 15, 2023 · 5 comments
Closed

Can't wrap the Query calls in another method. #69

lukemurray opened this issue Aug 15, 2023 · 5 comments
Labels
bug Something isn't working

Comments

@lukemurray
Copy link

Describe the bug

Can't wrap the Query calls in another method.

How to Reproduce

We're trying to replace an existing implementation and wanted to limit the changes across the code base. So we have the following

public async Task<GraphQLResult<TQueryResult>> QueryAsync<TQueryResult>(string operationName, Func<Query, TQueryResult> query)
        {
            var gqlClient = // .. make generated client with our IHttpHandler
            // do other things we need
            var res = await gqlClient.Query(operationName, query);
            return res;
        }

The await gqlClient.Query(operationName, query); gives the following error

Source generator failed unexpectedly with exception message:
Sequence contains no matching element
   at System.Linq.ThrowHelper.ThrowNoMatchException()
   at ZeroQL.SourceGenerators.Resolver.Context.GraphQLLambdaLikeContextResolver.Resolve(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken)
   at ZeroQL.SourceGenerators.Generator.GraphQLLambdaIncrementalSourceGenerator.GenerateFile(SourceProductionContext context, InvocationExpressionSyntax invocation, SemanticModel semanticModel, HashSet`1 processed)
   at ZeroQL.SourceGenerators.Generator.GraphQLLambdaIncrementalSourceGenerator.<>c__DisplayClass1_1.<GenerateSource>b__0()
   at ZeroQL.SourceGenerators.Utils.ErrorWrapper(SourceProductionContext context, CSharpSyntaxNode location, Action action)
Roslyn(ZQL0001)

Expected behavior

Clearly a Source Generator issue, but I'm not sure if it is a limitation of Source Generators or if it could be supported. If it is a limitation of Source Generators then you can close this. Otherwise it would be good to support this.

Environment (please complete the following information):

  • Nuget package version - 6.0.0
  • IDE: VS Code on Mac OS
  • .Net Version - 6
@lukemurray lukemurray added the bug Something isn't working label Aug 15, 2023
@jenschude
Copy link
Contributor

jenschude commented Aug 16, 2023

I tried something similar with the QueryInfoProvider. But you have to fetch the CallerArgumentExpression in the wrapper method else it will not work:

    public static void Query<TQuery>(Func<TQuery, object> query, [CallerArgumentExpression("query")] string queryKey = "")
    {
        var queryInfo = QueryInfoProvider.Materialize<TQuery>(query, queryKey);
    }

Also the queryKey is never set at runtime, it's important at compile time as the SourceGenerator from ZeroQL analyzes to build the query string and the query store. And retrieve the correct query string at runtime.

Edit: But it didn't worked out nicely. So i resorted to create an extension method which returned custom client with the IHttpHandler

public static class GraphQlClientExtensions
{
    // ReSharper disable once InconsistentNaming
    public static GraphQLClient GraphQLClient(this IClient client, string projectKey)
    {
        return new GraphQLClient(client.ToHandler(projectKey));
    }

    private static IHttpHandler ToHandler(this IClient client, string projectKey)
    {
        return new ClientHandler(client, projectKey);
    }
}

internal class ClientHandler : IHttpHandler
{
    private readonly IClient _client;
    private readonly Uri _graphQlUri;

    public ClientHandler(IClient client, string projectKey)
    {
        _client = client;
        _graphQlUri = new Uri($"/{projectKey}/graphql", UriKind.Relative);
    }

    public void Dispose()
    {
    }

    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.RequestUri = _graphQlUri;
        return await _client.SendAsAsync(request, cancellationToken);
    }
}

@byme8
Copy link
Owner

byme8 commented Aug 16, 2023

Theoretically, there is a way to make it work. It would require changes in analyzer and it would not look nice.
The QueryAsync would be changed like that:

       public async Task<GraphQLResult<TQueryResult>> QueryAsync<TQueryResult>(string operationName, [GraphQLLambda]Func<Query, TQueryResult> query, [CallerArgumentExpression(nameof(query))] string queryKey = null!)
        {
            var gqlClient = // .. make generated client with our IHttpHandler
            // do other things we need
            var res = await gqlClient.Query(operationName, query, queryKey);
            return res;
        }

and I have a feeling that such implementation would definitely get some sort of unknown WTF.

@jenschude
Copy link
Contributor

It would already help if there is a way that the analyzers capture classes implementing an interface provided by ZeroQL so libraries could implement it and read the query body from the QueryInfoProvider instead of using the generated ZeroQL client. Would have also the nice side effect to get the tooltips with the compiled query.

This would also ensure that the necessary functionality is supported like variable capturing etc.

@byme8
Copy link
Owner

byme8 commented Aug 16, 2023

Hmm.. Potentially the interface can be even a better way to implement it. I will have a look.

@byme8
Copy link
Owner

byme8 commented May 26, 2024

Checkout v7.0.0-.preview.1. Now it is possible to create wrappers around generated ZeroQL client. Here example:

public static async Task<T?> MakeQuery<T, TQuery, TMutation>(
        GraphQLClient<TQuery, TMutation> client, 
        [GraphQLLambda]Func<TQuery, T> query,
        [CallerArgumentExpression(nameof(query))] string queryKey = "")
        where T : class
{
    var result = await client.Query(query, queryKey: queryKey);
    
    if (result.Errors != null)
    {
        Console.WriteLine("Errors:");
        foreach (var error in result.Errors)
        {
            Console.WriteLine(error.Message);
        }

        return null;
    }
    
    return result.Data;
}

// or like that

public class GraphQLClientWrapper
{
    private readonly TestServerClient client;

    public GraphQLClientWrapper(TestServerClient client)
    {
        this.client = client;
    }

    public async Task<TResult> QueryAsync<TResult>(
        [GraphQLLambda]
        Func<Query, TResult> query,
        [CallerArgumentExpression(nameof(query))]
        string queryKey = "")
        where TResult : class
    {
        var result = await client.Query(query, queryKey: queryKey);

        return result.Data!;
    }
}

Basically new implementation depends on [GraphQLLambda] attribute. Every argument with it will be parsed and converted into query on build.

@byme8 byme8 closed this as completed May 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants