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.
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.
public class Query :
QueryGraphType<MyDbContext>
{
public Query(IEfGraphQLService<MyDbContext> graphQlService) :
base(graphQlService)
{
AddSingleField(
resolve: _ => _.DbContext.Companies,
name: "company");
AddQueryField(
name: "companies",
resolve: _ => _.DbContext.Companies);
}
}
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.
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();
}
}
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.
public class Query :
QueryGraphType<MyDbContext>
{
public Query(IEfGraphQLService<MyDbContext> graphQlService)
:
base(graphQlService) =>
AddQueryConnectionField<Company>(
name: "companies",
resolve: _ => _.DbContext.Companies.OrderBy(_ => _.Name));
}
{
companies(first: 2, after: "1") {
totalCount
edges {
node {
id
content
employees {
id
content
}
}
cursor
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
{
"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
}
}
}
}
public class CompanyGraph :
EfObjectGraphType<MyDbContext, Company>
{
public CompanyGraph(IEfGraphQLService<MyDbContext> graphQlService) :
base(graphQlService) =>
AddNavigationConnectionField(
name: "employees",
resolve: _ => _.Source.Employees);
}
public class DayOfTheWeekGraph : EnumerationGraphType<DayOfTheWeek>
{
}
public class ExampleGraph : ObjectGraphType<Example>
{
public ExampleGraph()
{
Field(x => x.DayOfTheWeek, type: typeof(DayOfTheWeekGraph));
}
}
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.
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 usingExpressionBuilder<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),
};
});
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);
});
}
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.