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

Expose the "Query" and "Mutation" extension methods on the IGraphqlClient iterface rather than the concrete type #95

Closed
be1223 opened this issue Feb 14, 2024 · 3 comments
Labels
enhancement New feature or request

Comments

@be1223
Copy link

be1223 commented Feb 14, 2024

Right now you can only use the "lambda" syntax on the concrete graphQLClient, it would be nice if this could be used on the interface:

IGraphQLClient client = new MyGeneratedZeroQLClient(new HttpClient());

var fooIds = await client.Query(static x => x.GetFoos(f => f.Id));

var variables = new { name = "some-name" };
var newFooId = await client.Mutation(variables, static (v, m) => m.CreateFoo(v.name, f => f.Id));

At present the only callable method on IGraphqlClient is Execute forcing us down the "Request" syntax route.

I explored the lambda syntax as a workaround for bug #94 and was forced to pass in the generated concrete client into my class constructor, moving away from the interface reference that I currently used.

@be1223 be1223 added the enhancement New feature or request label Feb 14, 2024
@byme8
Copy link
Owner

byme8 commented Feb 14, 2024

At the moment, we need the concrete implementation because it contains references to TQuery, TMutation. Then, the extension methods Query and Mutation expose them as part of the lambda.

public class GraphQLClient<TQuery, TMutation> : IGraphQLClient, IDisposable
{
  // ...
}

public static async Task<GraphQLResult<TResult>> Query<TVariables, TQuery, TMutation, TResult>(
        this GraphQLClient<TQuery, TMutation> client,
        string name,
        TVariables variables,
        Func<TVariables, TQuery, TResult> query,
        CancellationToken cancellationToken = default,
        [CallerArgumentExpression(nameof(query))] string queryKey = null!)
    {
      //..
    }

We can move it to the interface level, but then you will need to manually pass the correct TQuery and TMutation. From my perspective, you don't want to do it.

was forced to pass in the generated concrete client into my class constructor, moving away from the interface reference that I currently used.

What is the problem with the concrete client inside the class constructor?

@tommcdo
Copy link

tommcdo commented Mar 6, 2024

I wonder if an intermediate interface could help here:

public interface IGraphQLClient { ... }

public interface IGraphQLClient<TQuery, TMutation> : IGraphQLClient { ... }

public class GraphQLClient<TQuery, TMutation> : IGraphQLClient<TQuery, TMutation> { ... }

That said, Query is still an extension method, so it can't be mocked with common tools (such as Moq). Does it need to be an extension method?

As for getting around this now, it might actually be easier to mock interactions with the HttpClient than to mock the IGraphQLClient. Consider this rough example:

public class WidgetService
{
    private readonly ConcreteZeroGraphQLClient _client;

    public QueryService(ConcreteZeroGraphQLClient client) { _client = client; }

    public async Task<int> GetLengthOfLargestWidgetAsync()
    {
        IEnumerable<Widget> widgets = _client.Query(...);
        return widgets.Max(w => w.Length);
    }
}
public class WidgetServiceTests
{
    [Test]
    public async Task GetLengthOfLargestWidget_WhenGraphQLClientReturnsLotsOfResults_ReturnsLargest()
    {
        // Arrange
        var mockHttpClient = CreateHttpClientMock(); // Out of scope for this demo
        mockHttpClient
            .Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>, It.IsAny<CancellationToken>())
            .ReturnsAsync(new HttpResponseMessage
            {
                Content = new StringContent("""
                    {
                        "widgets": [
                            { "name": "foo", "length": 12 },
                            { "name": "bar", "length": 6 },
                            { "name": "baz", "length": 8 }
                        ]
                    }
                """)
            });

        var graphQLClient = new ConcreteGraphQLClient(mockHttpClient.Object);
        var widgetService = new WidgetService(graphQLClient);

        // Act
        var largest = await widgetService.GetLengthOfLargestWidgetAsync();

        // Assert
        largest.Should().Be(12);
    }
}

@byme8
Copy link
Owner

byme8 commented May 26, 2024

Checkout v7.0.0-preview.1

Now there is a way to create wrapper around the initial ZeroQL client and it works on interface level too. Here example how to create wrapper interface, fake it and use without server:

var httpClient = new HttpClient
{
    BaseAddress = new Uri("http://localhost:10000/graphql")
};

var zeroQlClient = new UserZeroQLClient(httpClient);
var wrapper = new UserGraphQlClient(zeroQlClient);

var fakeInterface = A.Fake<IUserGraphQLClient>();

A.CallTo(() => fakeInterface.QueryAsync(A<Func<Query, User>>.Ignored, A<string>.Ignored))
    .Returns(new User(new ID("FAKE_1"), "FAKE_FIRST_NAME", "FAKE_LAST_NAME"));

var serviceWithFake = new SomeService(fakeInterface);
var serviceWithReal = new SomeService(wrapper);

var fakeUser = await serviceWithFake.GetCurrentUser();
var realUser = await serviceWithReal.GetCurrentUser();

Console.WriteLine(JsonSerializer.Serialize(fakeUser));
// {"Id":{"Value":"FAKE_1"},"FirstName":"FAKE_FIRST_NAME","LastName":"FAKE_LAST_NAME"}
Console.WriteLine(JsonSerializer.Serialize(realUser));
// {"Id":{"Value":"1"},"FirstName":"John","LastName":"Smith"}

public record User(ID Id, string FirstName, string LastName);

public class SomeService(IUserGraphQLClient wrapper)
{
    public async Task<User?> GetCurrentUser()
    {
       // here we are doing query purely on top of interface
        var response = await wrapper.QueryAsync(q => q
            .Me(u => new User(u.Id, u.FirstName, u.LastName)));

        return response;
    }
}

public interface IUserGraphQLClient
{
    Task<TResult?> QueryAsync<TResult>(
        [GraphQLLambda] Func<Query, TResult> query,
        [CallerArgumentExpression(nameof(query))]
        string queryKey = "");
}

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

        return result.Data!;
    }
}

@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
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants