Closed be1223 closed 5 months ago
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?
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);
}
}
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!;
}
}
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:
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.