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

Create Query Error #4

Open
agriffard opened this issue Jul 13, 2020 · 6 comments
Open

Create Query Error #4

agriffard opened this issue Jul 13, 2020 · 6 comments
Assignees

Comments

@agriffard
Copy link

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
Copy link
Contributor

mkmarek commented Jul 13, 2020

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)

@mkmarek mkmarek self-assigned this Jul 13, 2020
@agriffard
Copy link
Author

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
Copy link
Contributor

mkmarek commented Jul 13, 2020

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
Copy link
Author

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
Copy link
Contributor

mkmarek commented Jul 28, 2020

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
Copy link
Author

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"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants