Skip to content

Latest commit

 

History

History
375 lines (301 loc) · 12.9 KB

defining-graphs.md

File metadata and controls

375 lines (301 loc) · 12.9 KB

Defining Graphs

Includes and Navigation properties.

Entity Framework has the concept of Navigation Properties:

A property defined on the principal and/or dependent entity that contains a reference(s) to the related entity(s).

In the context of GraphQL, Root Graph is the entry point to performing the initial EF query. Nested graphs then usually access navigation properties to return data, or perform a new EF query. New EF queries can be performed with AddQueryField and AddQueryConnectionField. Navigation properties queries are performed using AddNavigationField and AddNavigationConnectionField. For the above *ConnectionField refer to the GraphQL concept of pagination using Connections.

When performing a query there are several approaches to Loading Related Data

  • Eager loading means that the related data is loaded from the database as part of the initial query.
  • Explicit loading means that the related data is explicitly loaded from the database at a later time.
  • Lazy loading means that the related data is transparently loaded from the database when the navigation property is accessed.

Ideally, all navigation properties would be eagerly loaded as part of the root query. However determining what navigation properties to eagerly is difficult in the context of GraphQL. The reason is, given the returned hierarchy of data is dynamically defined by the requesting client, the root query cannot know what properties to include. To work around this GraphQL.EntityFramework interrogates the incoming query to derive the includes. So for example take the following query

{
  hero {
    name
    friends {
      name
      address {
        town
      }
    }
  }
}

Would result in the following query being performed

context.Heros
        .Include("Friends")
        .Include("Friends.Address");

The string for the include is taken from the field name when using AddNavigationField or AddNavigationConnectionField with the first character upper cased. This value can be overridden using the optional parameter includeNames . Note that includeNames is an IEnumerable<string> so that multiple navigation properties can optionally be included for a single node.

Fields

Queries in GraphQL.net are defined using the Fields API. Fields can be mapped to Entity Framework by using IEfGraphQLService. IEfGraphQLService can be used in either a root query or a nested query via dependency injection. Alternatively convenience methods are exposed on the types EfObjectGraphType or EfObjectGraphType<TSource> for root or nested graphs respectively. The below samples all use the base type approach as it results in slightly less code.

Root Query

public class Query :
    QueryGraphType<MyDbContext>
{
    public Query(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService)
    {
        AddSingleField(
            resolve: _ => _.DbContext.Companies,
            name: "company");
        AddQueryField(
            name: "companies",
            resolve: _ => _.DbContext.Companies);
    }
}

snippet source | anchor

AddQueryField will result in all matching being found and returned.

AddSingleField will result in a single matching being found and returned. This approach uses IQueryable<T>.SingleOrDefaultAsync as such, if no records are found a null will be returned, and if multiple records match then an exception will be thrown.

Typed Graph

public class CompanyGraph :
    EfObjectGraphType<MyDbContext,Company>
{
    public CompanyGraph(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService)
    {
        AddNavigationListField(
            name: "employees",
            resolve: _ => _.Source.Employees);
        AddNavigationConnectionField(
            name: "employeesConnection",
            resolve: _ => _.Source.Employees,
            includeNames: ["Employees"]);
        AutoMap();
    }
}

snippet source | anchor

Connections

Creating a page-able field is supported through GraphQL Connections by calling IEfGraphQLService.AddNavigationConnectionField (for an EF navigation property), or IEfGraphQLService.AddQueryConnectionField (for an IQueryable). Alternatively convenience methods are exposed on the types EfObjectGraphType or EfObjectGraphType<TSource> for root or nested graphs respectively.

Root Query

Graph Type

public class Query :
    QueryGraphType<MyDbContext>
{
    public Query(IEfGraphQLService<MyDbContext> graphQlService)
        :
        base(graphQlService) =>
        AddQueryConnectionField<Company>(
            name: "companies",
            resolve: _ => _.DbContext.Companies.OrderBy(_ => _.Name));
}

snippet source | anchor

Request

{
  companies(first: 2, after: "1") {
    totalCount
    edges {
      node {
        id
        content
        employees {
          id
          content
        }
      }
      cursor
    }
    pageInfo {
      startCursor
      endCursor
      hasPreviousPage
      hasNextPage
    }
  }
}

Response

{
  "data": {
    "companies": {
      "totalCount": 4,
      "edges": [
        {
          "node": {
            "id": "1",
            "content": "Company1",
            "employees": [
              {
                "id": "2",
                "content": "Employee1"
              },
              {
                "id": "3",
                "content": "Employee2"
              }
            ]
          },
          "cursor": "1"
        },
        {
          "node": {
            "id": "4",
            "content": "Company3",
            "employees": []
          },
          "cursor": "2"
        }
      ],
      "pageInfo": {
        "startCursor": "1",
        "endCursor": "2",
        "hasPreviousPage": true,
        "hasNextPage": true
      }
    }
  }
}

Typed Graph

public class CompanyGraph :
    EfObjectGraphType<MyDbContext, Company>
{
    public CompanyGraph(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        AddNavigationConnectionField(
            name: "employees",
            resolve: _ => _.Source.Employees);
}

snippet source | anchor

Enums

public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
{
}
public class ExampleGraph : ObjectGraphType<Example>
{
    public ExampleGraph()
    {
        Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
    }
}

AutoMap

Mapper.AutoMap can be used to remove repetitive code by mapping all properties of a type.

For example for this graph:

public class EmployeeGraph :
    EfObjectGraphType<SampleDbContext, Employee>
{
    public EmployeeGraph(IEfGraphQLService<SampleDbContext> graphQlService) :
        base(graphQlService)
    {
        AddNavigationField(
            name: "company",
            resolve: context => context.Source.Company);
        Field(employee => employee.Age);
        Field(employee => employee.Content);
        Field(employee => employee.CompanyId);
        Field(employee => employee.Id);
    }
}

The equivalent graph using AutoMap is:

public class EmployeeGraph :
    EfObjectGraphType<SampleDbContext, Employee>
{
    public EmployeeGraph(IEfGraphQLService<SampleDbContext> graphQlService) :
        base(graphQlService)
    {
        AutoMap();
    }
}

The underlying behavior of AutoMap is:

  • Calls IEfGraphQLService{TDbContext}.AddNavigationField{TSource,TReturn} for all non-list EF navigation properties.
  • Calls IEfGraphQLService{TDbContext}.AddNavigationListField{TSource,TReturn} for all EF navigation properties.
  • Calls ComplexGraphType{TSourceType}.AddField for all other properties

An optional list of exclusions can be passed to exclude a subset of properties from mapping.

Mapper.AddIgnoredType can be used to exclude properties (of a certain type) from mapping.

Manually Apply WhereExpression

In some cases, it may be necessary to use Field instead of AddQueryField/AddSingleField/etc but still would like to use apply the where argument. This can be useful when the returned Graph type is not for an entity (for example, aggregate results). To support this:

  • Add the WhereExpressionGraph argument
  • Apply the where argument expression using ExpressionBuilder<T>.BuildPredicate(whereExpression)

Field<ListGraphType<EmployeeSummaryGraphType>>("employeeSummary")
    .Argument<ListGraphType<WhereExpressionGraph>>("where")
    .Resolve(context =>
    {
        var dbContext = ResolveDbContext(context);
        IQueryable<Employee> query = dbContext.Employees;

        if (context.HasArgument("where"))
        {
            var wheres = context.GetArgument<List<WhereExpression>>("where");

            var predicate = ExpressionBuilder<Employee>.BuildPredicate(wheres);
            query = query.Where(predicate);
        }

        return from q in query
            group q by new
            {
                q.CompanyId
            }
            into g
            select new EmployeeSummary
            {
                CompanyId = g.Key.CompanyId,
                AverageAge = g.Average(_ => _.Age),
            };
    });

snippet source | anchor

Resolving DbContext

Sometimes it is necessary to access the current DbContext from withing the base QueryGraphType.Field method. in this case the custom ResolveEfFieldContext is not available. In this scenario QueryGraphType.ResolveDbContext can be used to resolve the current DbContext.

public class Query :
    QueryGraphType<MyDbContext>
{
    public Query(IEfGraphQLService<MyDbContext> graphQlService) :
        base(graphQlService) =>
        Field<ListGraphType<CompanyGraph>>("oldCompanies")
            .Resolve(context =>
            {
                // uses the base QueryGraphType to resolve the db context
                var dbContext = ResolveDbContext(context);
                return dbContext.Companies.Where(_ => _.Age > 10);
            });
}

snippet source | anchor

ArgumentProcessor

ArgumentProcessor (via the method ApplyGraphQlArguments) is responsible for extracting the various parts of the GraphQL query argument and applying them to an IQueryable<T>. So, for example, each where argument is mapped to a IQueryable.Where and each skip argument is mapped to a IQueryable.Where.

The arguments are parsed and mapped each time a query is executer.

ArgumentProcessor is generally considered an internal API and not for public use. However there are some advanced scenarios, for example when building subscriptions, that ArgumentProcessor is useful.