telia-oss / graphql-typed-client

Strongly typed GraphQL client for .NET
MIT License
20 stars 3 forks source link

Create Query Error #4

Open agriffard opened 4 years ago

agriffard commented 4 years ago

When I try to create a query with a DateTime GraphQLField, an error occurs.

Example with a schema like this one:

interface ContentItem {
  contentItemId: String!
  contentItemVersionId: String!
  contentType: String!
  displayText: String
  published: Boolean!
  latest: Boolean!
  modifiedUtc: DateTime
  publishedUtc: DateTime
  createdUtc: DateTime
  owner: String!
  author: String!
}

type WeatherForecast implements ContentItem {
  contentItemId: String!
  contentItemVersionId: String!
  contentType: String!
  displayText: String
  published: Boolean!
  latest: Boolean!
  modifiedUtc: DateTime
  publishedUtc: DateTime
  createdUtc: DateTime
  owner: String!
  author: String!
  render: String
  temperature: Decimal
  summary: String
}

type Query {
  weatherForecast(where: WeatherForecastWhereInput): [WeatherForecast]
}

input WeatherForecastWhereInput {
  createdUtc_gt: DateTime
}  

If I try to call a query like this one:

                var query = client.CreateQuery<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
                   new WeatherForecastWhereInput() { CreatedUtcGt = DateTime.UtcNow }));

A NullReferenceException occurs: Object reference not set to an instance of an object

   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromMemberAccessExpression(MemberExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromUnaryExpression(UnaryExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromMemberInit(MemberInitExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.<GetArgumentsFromMethod>d__9.MoveNext()
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)
   at Telia.GraphQL.Client.ChainLink.Equals(Object obj)
   at Telia.GraphQL.Client.SelectionChainGrouping.<>c__DisplayClass3_0.<TryGroup>b__0(ChainLink e)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at Telia.GraphQL.Client.SelectionChainGrouping.TryGroup(ChainLink part, List`1 groupedLink)
   at Telia.GraphQL.Client.SelectionChainGrouping.Group()
   at Telia.GraphQL.GraphQLCLient`1.CreateOperation[TType,TReturn](Expression`1 selector, QueryContext context, OperationType operationType)
   at Telia.GraphQL.GraphQLCLient`1.CreateQuery[TReturn](Expression`1 selector)   
mkmarek commented 4 years ago

Hi, thanks for reporting this.

It looks like one of the expression visitors extracting what fields you're trying to access from the schema doesn't like the DateTime.UtcNow part. What you could do to at least make it work is this:

var now = DateTime.UtcNow;
var query = client.CreateQuery(s => s.WeatherForecast(
  new WeatherForecastWhereInput() { CreatedUtcGt = now }));

I can have a look later today for more general fix on this. (In about 4 hours from now)

agriffard commented 4 years ago

Thank you for your answer.

Can you please tell me how a And works?

This code:

var startDateUtc = DateTime.Today.ToUniversalTime();
var endDateUtc = startDateUtc.AddDays(1);

                var query = client.CreateQuery<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
                    new WeatherForecastWhereInput() { And = new List<WeatherForecastWhereInput>() {
                        new WeatherForecastWhereInput() { CreatedUtcGte = startDateUtc },
                        new WeatherForecastWhereInput() { CreatedUtcLt = endDateUtc } }
                    }));

returns this NotImplementedException: GetValueFromExpression: unknown NodeType: ListInit

   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromMemberInit(MemberInitExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.<GetArgumentsFromMethod>d__9.MoveNext()
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)
   at Telia.GraphQL.Client.ChainLink.Equals(Object obj)
   at Telia.GraphQL.Client.SelectionChainGrouping.<>c__DisplayClass3_0.<TryGroup>b__0(ChainLink e)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at Telia.GraphQL.Client.SelectionChainGrouping.TryGroup(ChainLink part, List`1 groupedLink)
   at Telia.GraphQL.Client.SelectionChainGrouping.Group()
   at Telia.GraphQL.GraphQLCLient`1.CreateOperation[TType,TReturn](Expression`1 selector, QueryContext context, OperationType operationType)
   at Telia.GraphQL.GraphQLCLient`1.CreateQuery[TReturn](Expression`1 selector)
mkmarek commented 4 years ago

In general the library looks at everything you put into the expression and tries to inline it into a GraphQL query. So it actually crawls the expression tree, recomposes it into its own data model and then it optimizes the query.

For example if you use the same field twice in the query then it merges it into one. So this:

var q = client.CreateQuery(s => s.WeatherForecast().Select(e => new
            {
                x = e.Author,
                y = $"{e.Owner}/{e.Author}"
            }));

Will result into:

{
  field0: weatherForecast{
    field0: author
    field1: owner
    __typename
  }
  __typename
}

Since it identified that the Author field is pointing to the same data it won't duplicate it in the query but when it maps it back to your C# types it will distribute it accordingly.

When it comes to input objects they are a bit tricky with lots of corner cases. In your case List constructor inside an input is not supported. Array initialization is.

So if you would use array initialization like this:

var query = client.CreateQuery<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
                new WeatherForecastWhereInput()
                {
                    And = new WeatherForecastWhereInput[] {
                        new WeatherForecastWhereInput() { CreatedUtcGte = startDateUtc },
                        new WeatherForecastWhereInput() { CreatedUtcLt = endDateUtc } }
                }));

It should yield you this query:

{
  field0: weatherForecast(where: {and: [{createdUtc_gte: "2020-07-12T22:00:00Z"}, {createdUtc_lt: "2020-07-13T22:00:00Z"}]}){
    contentItemId
    contentItemVersionId
    contentType
    displayText
    published
    latest
    modifiedUtc
    publishedUtc
    createdUtc
    owner
    author
    render
    temperature
    summary
    __typename
  }
  __typename
}

Sadly it won't right now because of another probably recently introduced bug. I'm contemplating about just doing JSON serialization on input objects in general and passing them in the result query as variables, which is probably what I'll end up doing. That should solve whole lot of problems. I'll add that on my list.

agriffard commented 4 years ago

Ok, thank you for the explanations.

Right now with an Array, it results to an InvalidCastException: Object cannot be stored in an array of this type.

   at System.Array.InternalSetValue(Void* target, Object value)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromNewArrayInit(NewArrayExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromMemberInit(MemberInitExpression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.GetValueFromExpression(Expression argument)
   at Telia.GraphQL.Client.PathGatheringVisitor.<GetArgumentsFromMethod>d__9.MoveNext()
   at System.Linq.Enumerable.Count[TSource](IEnumerable`1 source)
   at Telia.GraphQL.Client.ChainLink.Equals(Object obj)
   at Telia.GraphQL.Client.SelectionChainGrouping.<>c__DisplayClass3_0.<TryGroup>b__0(ChainLink e)
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at Telia.GraphQL.Client.SelectionChainGrouping.TryGroup(ChainLink part, List`1 groupedLink)
   at Telia.GraphQL.Client.SelectionChainGrouping.Group()
   at Telia.GraphQL.GraphQLCLient`1.CreateOperation[TType,TReturn](Expression`1 selector, QueryContext context, OperationType operationType)
   at Telia.GraphQL.GraphQLCLient`1.Query[TReturn](Expression`1 selector)
mkmarek commented 4 years ago

Hi, sorry for the delay. I finally got around to implement hopefully all the necessary fixes.

For your initial query:

var query = client.CreateQuery<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
                   new WeatherForecastWhereInput() { CreatedUtcGt = DateTime.UtcNow }));

It now creates not string but rather an object of type GraphQLQueryInfo containing the query itself and all variables passed in. Instead of trying to inline the input values into the query it just evaluates them and puts the resulting value in as a variable. So if you would create something like this:

var client = new Client();

var query = client.CreateQuery(s => s.WeatherForecast(
  new WeatherForecastWhereInput() { CreatedUtcGt = DateTime.UtcNow }));

var json = JsonConvert.SerializeObject(query, new JsonSerializerSettings()
  {
    Converters = new List<JsonConverter>()
    {
      new GraphQLObjectConverter() // in order to maintain the same naming of input values this converter needs to be added
    },
    Formatting = Formatting.Indented
});

It will result into the following JSON:

{
  "query": "query Query($var_0: WeatherForecastWhereInput) {\r\n  field0: weatherForecast(where: $var_0){\r\n    contentItemId\r\n    contentItemVersionId\r\n    contentType\r\n    displayText\r\n    published\r\n    latest\r\n    modifiedUtc\r\n    publishedUtc\r\n    createdUtc\r\n    owner\r\n    author\r\n    render\r\n    temperature\r\n    summary\r\n    __typename\r\n  }\r\n  __typename\r\n}",
  "variables": {
    "var_0": {
      "createdUtc_gt": "2020-07-28T08:06:36.0050729Z"
    }
  }
}

Similar thing will happen with this query:

var query = client.CreateQuery<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
  new WeatherForecastWhereInput()
  {
    And = new List<WeatherForecastWhereInput>() {
      new WeatherForecastWhereInput() { CreatedUtcGte = startDateUtc },
      new WeatherForecastWhereInput() { CreatedUtcLt = endDateUtc } }
}));

Which results into:

{
  "query": "query Query($var_0: WeatherForecastWhereInput) {\r\n  field0: weatherForecast(where: $var_0){\r\n    contentItemId\r\n    contentItemVersionId\r\n    contentType\r\n    displayText\r\n    published\r\n    latest\r\n    modifiedUtc\r\n    publishedUtc\r\n    createdUtc\r\n    owner\r\n    author\r\n    render\r\n    temperature\r\n    summary\r\n    __typename\r\n  }\r\n  __typename\r\n}",
  "variables": {
    "var_0": {
      "and": [
        {
          "CreatedUtcGte": "2020-07-27T22:00:00Z"
        },
        {
          "CreatedUtcLt": "2020-07-28T22:00:00Z"
        }
      ]
    }
  }
}

This required some additional type info to be added into the schema cs file so it knows what kind of graphql type it is you're trying to send in as that variable. So to use these fixes you might want to download the latest version of https://marketplace.visualstudio.com/items?itemName=MarekMagdziak.Telia-GraphQL-Tooling&ssr=false#overview

Version 1.1.40 of the NuGet package and the extension contains all the fixes.

Edit: Just found out that Newtonsoft JSON converts enums as integers as default so you might want to also add new StringEnumConverter to your converter list.

agriffard commented 4 years ago

OK, about the enum serialiazation, when I am querying this:

var result = client.Query<IEnumerable<WeatherForecast>>(s => s.WeatherForecast(
                    new WeatherForecastWhereInput()
                    {
                        And = new WeatherForecastWhereInput[] {
                        new WeatherForecastWhereInput() { CreatedUtcGte = startDateUtc },
                        new WeatherForecastWhereInput() { CreatedUtcLt = endDateUtc } }
                    },
                    new WeatherForecastOrderByInput() { CreatedUtc = OrderByDirection.ASC })
                );

OrderByDirection being an enum:

[GraphQLType("OrderByDirection")]
    public enum OrderByDirection
    {
        ASC,
        DESC
    }

It sends a query like this: {"query":"query Query($var_0: WeatherForecastWhereInput, $var_1: WeatherForecastOrderByInput) {\r\n field0: weatherForecast(where: $var_0, orderBy: $var_1){\r\n contentItemId\r\n contentItemVersionId\r\n contentType\r\n displayText\r\n published\r\n latest\r\n modifiedUtc\r\n publishedUtc\r\n createdUtc\r\n owner\r\n author\r\n render\r\n date\r\n temperature\r\n summary\r\n __typename\r\n }\r\n __typename\r\n}","variables":{"var_0":{"AND":[{"createdUtc_gte":"2020-07-27T22:00:00Z"},{"createdUtc_lt":"2020-07-30T22:00:00Z"}]},"var_1":{"createdUtc":0}}}

Note the last variable: "var_1":{"createdUtc":0} should be "var_1":{"createdUtc":"ASC"}