Getting Started
EntityGraphQL is a .NET library for building a GraphQL API on top of your data model, with the extensibility to bring multiple data sources together in a single GraphQL schema. Your schema maps to plain .NET objects - an Entity Framework DbContext or any other object graph.
A core feature (with EF, although EF is not a requirement) is that queries are compiled to LINQ Select() projections of only the fields requested in the GraphQL query - Entity Framework doesn't return all columns of a table, and it works across any object tree.
Installation
If you are working with ASP.NET then install EntityGraphQL.AspNet via Nuget. It will allow you to:
- Quickly get started with ASP.NET
- Integrate with ASP.NET policy authorization
You can install the core EntityGraphQL package if you do not need ASP.NET.
Create a data model
Note: There is no dependency on Entity Framework. Queries are compiled to IQueryable or IEnumerable LINQ expressions. EF is not a requirement - any LINQ provider based ORM or in-memory objects will work - this example uses EF.
public class DemoContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
public DbSet<Person> People { get; set; }
}
public class Movie
{
public uint Id { get; set; }
public string Name { get; set; }
public Genre Genre { get; set; }
public DateTime Released { get; set; }
public Person Director { get; set; }
public uint? DirectorId { get; set; }
public double Rating { get; set; }
}
public enum Genre
{
Action,
Drama,
Comedy,
Horror,
Scifi,
}
public class Person
{
public uint Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Dob { get; set; }
public List<Movie> DirectorOf { get; set; }
}
Create the API
Using what ever .NET API library you wish you can receive a query, execute it and return the data. Here is an example with ASP.NET.
You will need to install EntityGraphQL.AspNet to use MapGraphQL<>() and AddGraphQLSchema(). You can also build your own endpoint, see below.
using EntityGraphQL.AspNet;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DemoContext>(opt => opt.UseInMemoryDatabase("Demo"));
builder.Services.AddGraphQLSchema<DemoContext>();
var app = builder.Build();
app.MapGraphQL<DemoContext>();
app.Run();
This sets up a HTTP POST end point at /graphql where the body of the post is expected to be a GraphQL query. You can change the path with the path argument in MapGraphQL<T>()
MapGraphQL follows the GraphQL over HTTP spec. GraphQL errors are returned in the response body - always read errors there. See Tool integration for the status codes used.
You can authorize the route how ever you wish using ASP.NET. See the Authorization section for more details.
You can also expose any endpoint over any protocol you'd like. We'll use HTTP/S for these examples.
Securing the route in ASP.NET core
When using MapGraphQL(), the route is added with the IEndpointRouteBuilder.MapPost method. The .MapPost() method can be chained with .RequireAuthorization() where an array of Policy Names can be passed. The policy names are ANDed together with .RequireAuthorization().
To add one or more security policies when using MapGraphQL() you can pass a configure function that has access to the IEndpointConventionBuilder from the created .MapPost().
//in ConfigureServices
services.AddAuthentication()
services.AddAuthorization(options =>
{
options.AddPolicy("authorized", policy => policy.RequireAuthenticatedUser());
});
//in Configure
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// defaults to /graphql endpoint
endpoints.MapGraphQL<DemoContext>(configureEndpoint: (endpoint) => {
endpoint.RequireAuthorization("authorized");
// do other things with endpoint
});
});
Query your API
You can now make a request to your API via any HTTP tool/library.
For example
POST localhost:5000/graphql
{
"query": "{
movies { id name }
}",
"variables": null
}
Will return the following result (depending on the data in you DB).
{
"data": {
"movies": [
{
"id": 11,
"name": "Inception"
},
{
"id": 12,
"name": "Star Wars: Episode IV - A New Hope"
}
]
}
}
Maybe you only want a specific property (request body only from now on)
{
movie(id: 11) {
id
name
}
}
Will return the following result.
{
"data": {
"movie": {
"id": 11,
"name": "Inception"
}
}
}
If you need other fields or relations, just ask
{
movies {
id
name
director {
firstName
lastName
}
}
}
Will return the following result.
{
"data": {
"movies": [
{
"id": 11,
"name": "Inception",
"director": {
"firstName": "Christopher",
"lastName": "Nolan"
}
},
{
"id": 12,
"name": "Star Wars: Episode IV - A New Hope",
"director": {
"firstName": "George",
"lastName": "Lucas"
}
}
]
}
}
Custom Controller / Manual Execution
You can execute GraphQL queries in your own controller or outside of ASP.NET. Below gives an example.
Build the GraphQL schema
We can use the helper method SchemaBuilder.FromObject<T>>() to build the schema from the .NET object model we defined above. Create this once at startup and register it with your services.
var schema = SchemaBuilder.FromObject<DemoContext>();
services.AddSingleton(schema);
See the Schema Creation section to learn more about SchemaBuilder.FromObject<T>>()
If you are already using EntityGraphQL.AspNet, prefer AddGraphQLSchema<T>() so the schema is registered consistently with the rest of the ASP.NET integration.
Executing a Query in a custom controller
Here is an example of a controller that receives a QueryRequest and executes the query. This logic could easily be applied to other web frameworks.
[Route("graphql")]
public class QueryController : Controller
{
private readonly SchemaProvider<DemoContext> _schemaProvider;
public QueryController(SchemaProvider<DemoContext> schemaProvider)
{
this._schemaProvider = schemaProvider;
}
[HttpPost]
public async Task<object> Post([FromBody]QueryRequest query)
{
var results = await _schemaProvider.ExecuteRequestAsync(query, HttpContext.RequestServices, HttpContext.User);
// gql compile errors show up in results.Errors
return results;
}
}
If you want to inspect or export the schema that your custom controller is serving, you can expose schema.ToGraphQLSchemaString() from a simple endpoint:
[Route("graphql-schema")]
public class SchemaController : Controller
{
private readonly SchemaProvider<DemoContext> _schemaProvider;
public SchemaController(SchemaProvider<DemoContext> schemaProvider)
{
_schemaProvider = schemaProvider;
}
[HttpGet]
public ContentResult Get()
{
return Content(_schemaProvider.ToGraphQLSchemaString(), "text/plain");
}
}
This is useful for verifying that the schema registered in DI contains the fields you expect and for generating SDL for tooling.
Executing a Query in azure functions (isolated)
Here is an example of a function that receives a HttpRequestData, deserializes a QueryRequest from the body, and executes it against a schema that is created once and reused.
public class GraphQLFunctions
{
private readonly DemoContext _dbContext;
private readonly SchemaProvider<DemoContext> _schemaProvider;
public GraphQLFunctions(DemoContext dbContext, SchemaProvider<DemoContext> schemaProvider)
{
_dbContext = dbContext;
_schemaProvider = schemaProvider;
}
[Function(nameof(GraphQL))]
public async Task<HttpResponseData> GraphQL([HttpTrigger(AuthorizationLevel.Function, "post", Route = "graphql")] HttpRequestData req)
{
var query = await req.GetJsonBody<QueryRequest>(); //helper method to deserialize QueryRequest from req.Body
var results = await _schemaProvider.ExecuteRequestWithContextAsync(query, _dbContext, null, null);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(results);
return response;
}
}
Configuring System.Text.Json
If you use your own controller/method to execute GraphQL and use System.Text.Json, it is best to configure it like below for best compatibility with other tools.
services.AddControllers()
.AddJsonOptions(opts =>
{
// Use enum field names instead of numbers
opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// EntityGraphQL internally builds types with fields
opts.JsonSerializerOptions.IncludeFields = true;
// The fields internally built already are named with fieldNamer (defaults to camelCase). This is
// for the properties on QueryResult (Data, Errors) to match what most tools etc expect (camelCase)
opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
Deserialization of QueryRequest & QueryVariables
If you are using your own controller/method to execute GraphQL and deserializing a GraphQL request like below into the QueryRequest object, you need to be aware of how your serializer handles nested Dictionary<string, object>.
Sample incoming json request
{
"query": "mutation Mutate($var: ComplexInputType){ doUpdate($var) }",
"variables": {
"var": {
"name": "Lisa",
"lastName": "Simpson"
}
}
}
Many deserializer libraries will deserialize this into the QueryRequest.Variables object with the value of var as a JsonElement (System.Text.Json) or a JObject (Newtonsoft.Json). e.g.
var gql = JsonSerializer.Deserialize<QueryRequest>(query);
Assert.True(gql.Variables["var"].GetType() == typeof(JsonElement));
What we want is a nested Dictionary<string, object>. EntityGraphQL handles System.Text.Json's JsonElement itself. However if you are using Newtonsoft.Json or another library (that doesn't deserialize to nested dictionaries) you will have to provide a custom type converter.
See the serialization tests for an example with Newtonsoft.Json.
Threading and async execution
EntityGraphQL compiles the whole GraphQL request document then selects the operation to execute. For a mutation operation all top-level mutations are executed individually, in the order they appear in the document, as required by GraphQL. For a query operation, each top-level query field also executes sequentially in document order. Concurrency happens within a field: async fields resolved for a list run per item concurrently (bounded by ExecutionOptions.MaxQueryConcurrency, default 100 — see Async Fields), and for async-capable LINQ providers like EF Core the database query is awaited asynchronously and honours the request's CancellationToken.
Since top-level fields execute sequentially, database contexts can be scoped services like they are for ordinary web services. Note that a scoped, non-thread-safe service (like a DbContext) used in an async field on a list will be called concurrently — see Async Fields for the concurrency options. Queries and mutations that call external web services can pass a CancellationToken through (MapGraphQL() uses HttpContext.RequestAborted) to cancel dependent work if the GraphQL request is aborted.
Async Fields
EntityGraphQL provides comprehensive support for asynchronous field resolution using the ResolveAsync method. This allows you to integrate with external services, APIs, and perform long-running operations while maintaining control over concurrency and performance.
// Basic async field with service injection
schema.Type<Person>()
.AddField("weather", "Current weather data")
.ResolveAsync<WeatherService>((person, weatherService) =>
weatherService.GetWeatherAsync(person.Location));
// With concurrency control
schema.Type<Person>()
.AddField("profile", "External profile data")
.ResolveAsync<ProfileService>((person, service) =>
service.GetProfileAsync(person.Id),
maxConcurrency: 5); // Limit to 5 concurrent operations
EntityGraphQL supports hierarchical concurrency control at field, service, and query levels to help you manage resource usage effectively.
For comprehensive information about async fields, concurrency control, error handling, and best practices, see Async Fields.
Tracking Argument Values: IArgumentsTracker
EntityGraphQL provides a way to help you determine if an argument or input property was explicitly set by the user in a query or mutation, or if it is just the default .NET value. This is useful for distinguishing between "not provided" and "provided as null/default".
IArgumentsTracker
If your argument or input class implements the IArgumentsTracker interface (or inherits from the provided ArgumentsTracker base class), you can check if a property was set by the user:
public class MyInput : ArgumentsTracker {
public string? Name { get; set; }
public int? Age { get; set; }
}
// For a field
schema.Query().AddField("test", new MyInput(), (db, args) => db.People.WhereWhen(p => args.Name == p.Name, args.IsSet(nameof(MyInput.Name))), "test field");
// In your mutation or subscriptions
public string MyMutation(MyInput input) {
if (input.IsSet(nameof(MyInput.Name))) {
// Name was provided in the query
}
if (!input.IsSet(nameof(MyInput.Age))) {
// Age was not provided
}
...
}
This works for both inline arguments and variables.
For simple argument lists (e.g. method parameters), you can add an IArgumentsTracker parameter to your mutation or query method. This allows you to check if a specific argument was set:
public string MyMutation(Guid? id, string? name, IArgumentsTracker argsTracker) {
if (argsTracker.IsSet(nameof(id))) {
// id was provided
}
if (!argsTracker.IsSet(nameof(name))) {
// name was not provided
}
...
}
This is especially useful for distinguishing between omitted arguments and those set to null/default.