Practical ASP.NET
Letting the Client Control Data Retrieval with GraphQL in .ASP.NET Core
GraphQL lets you create data access services without writing controllers. Instead of writing procedural code, you declare schemas describing what queries you'll accept and what you're willing to return. Here's how to get started in ASP.NET Core.
Thinking of GraphQL as being like SQL or LINQ isn't a bad mental model: Like SQL/LINQ, if you know the names of the entities, their properties and the relationships between entities, then you're free to craft any valid query. And, like views in SQL, your schema declaratively presents what you think will be helpful to your clients while controlling what you will allow them to do. But, just like when you write a SQL statement, you're in control of what you decide to retrieve from those views.
And, in the same way that you submit all your SQL queries against a single connection string (or all of your LINQ queries against a single DbContext object), a GraphQL client submits all of its queries to the same URL. To extend that SQL metaphor a little further, in the same way that adding new tables (or new columns to existing tables) doesn't break existing queries, adding new entities or properties to your GraphQL schema doesn't break any existing clients. One last extension: Like SQL, the code you write both to implement and use GraphQL is declarative -- you just keep telling GraphQL what you want and it takes care of making it happen.
But that's where the metaphor ends. GraphQL uses a query syntax that looks a lot like a JavaScript literal. A query to return the CustomerId and Name properties for all the customer entities looks like this (GraphQL automatically lowercases the first letter of all your entities' properties):
{customers{customerId, name}}
To send that query to a GraphQL-enabled service, you just tack it as a querystring value called "query" on to a service's GraphQL endpoint. By default that endpoint is <URL for the service>/graphql so, for this case study, the URL that submits that query to my service looks like this:
http://localhost:64447/graphql?query={customers{customerId, name}}
The result comes back as a JSON object:
{"data":{"customers":[
{"customerId":1,"name":"Peter Vogel"},
{"customerId":2,"name":"Jason Van de Velde"}
]}}
One last bit of good news: If you've ever created a Web API service that returns data, then you've got a service that's ready for GraphQL (see the sidebar "Creating a Data-Oriented Web Service").
Configuring a GraphQL Project
In the Startup.cs file for your Web API data service project, you need to add this code to the ConfigureServices method to both add and configure the GraphQL objects required in the application's services collection:
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddGraphQL(o => { o.ExposeExceptions = false; })
.AddGraphTypes(ServiceLifetime.Scoped);
Because of these configuration settings, GraphQL is eventually going to want to pull your customer repository from the services collection. To support that I added my CustomerRepository to the services collection, also:
services.AddScoped<CustomerRepository>();
What you don't need is a controller. Instead, you just need a GraphQL schema.
Defining the Schema
The first step in defining a GraphQL schema is to create an ObjectGraphType class, tied to the entity object you will return (Customer, in my case). The ObjectGraphType is like a SQL View in the sense that it defines what's available to the client and what valid queries look like.
To begin with, in the constructor for your ObjectGraphType, you define the fields available to the client. In my case, I'll make both of the properties on my Customer class available:
public class CustomerOGT : ObjectGraphType<Customer>
{
public CustomerOGT()
{
Field(c => c.CustomerId);
Field(c => c.Name);
}
Within that ObjectGraphType, you define queries using nested classes. These are, again, classes that inherit from ObjectGraphType (you don't need to specify an entity type here, because GraphQL will infer it). That class' constructor must ask for my repository object from the services collection. Because this query returns all my customer entity objects, I've called the class GetAllQuery:
public class CustomerOGT : ObjectGraphType<Customer>
{
public CustomerOGT()
{
Field(c => c.CustomerId);
Field(c => c.Name);
}
public class GetAllQuery : ObjectGraphType
{
public GetAllQuery(CustomerRepository custRepo)
{
Within the class' constructor, I call the ObjectGraphType's Field method, specifying what I want to be returned. I have a variety of types I can return, but I've chosen the ListGraphType because it's the simplest. I tie that ListGraphType to the parent class which specified, in its constructor, what fields will appear in the result.
I then pass to the Field method the string that identifies what queries this class will be used with ("helloWorld" in this case -- remember that GraphQL is going to lowercase the first letter of all your names so you might as well do that also). There are a number of other parameters that I can pass to the Field method, but the only one I must pass is the resolve parameter because it specifies what method in my repository will generate the result. I'll use a named parameter ("resolve") to skip over the Field method's other parameters and tie this query to the GetAll method I created on my customer repository.
Putting all that together, my call to the Field method inside my GetAllQuery's constructor looks like this:
public class GetAllQuery : ObjectGraphType
{
public GetAllQuery(CustomerRepository custRepo)
{
Field<ListGraphType<CustomerOGT>>(
"helloWorld",
resolve: context => custRepo.GetAll()
);
}
}
Notice the absence of any procedural logic here: I just keep declaring what's available in my schema.
The final part of your configuration is to create a GraphQL schema that makes your query available to GraphQL. The first step is to create a class that inherits from GraphQL's Schema class. In the constructor for that class you need to accept the DependencyResolver you added to the services collection in ConfigureServices (you also need to pass that resolver to the constructor for the base Schema class you're inheriting from). Inside the constructor, you tie that resolver to your query class using its Resolve method and store the result in the Schema class' Query property.
Here's my schema:
public class CustomerSchema: Schema
{
public CustomerSchema(IDependencyResolver resolver) : base(resolver)
{
Query = resolver.Resolve<GetAllQuery>();
}
}
Since the GetAllQuery class I reference inside my schema is nested inside my CustomerOGT class, I need a using statement that references that CustomerOGT class. Visual Studio is perfectly willing to add the necessary using statement, which looks like this:
using static GraphQLSample.Models.CustomerOGT;
Final Configuration
Finally, you have to tell your application about your schema. Back in my Startup.cs file, in the ConfigureServices method, I add my schema to the application's services collection:
services.AddScoped<CustomerSchema>();
In the Configure method, I add GraphQL to my application's processing pipeline and tell it to use my schema:
app.UseGraphQL<CustomerSchema>();
And now, I've tied GraphQL to my CustomerSchema whose Query is tied to my GetAllQuery class. This lets me issue a query against the helloWorld query that my GetAllQuery class contains. That query will use my repository to resolve (retrieve) the Customer entities. Because the GetAllQuery is nested inside my CustomerOGT, I can retrieve any of the fields CustomerOGT exposes. In this example, I've tailored my helloWorld query to get just the name field for both of my Customer entities:
http://localhost:64447/graphql?query={helloWorld{name}}
And I get back this result:
{"data":{"helloWorld":[{"name":"Peter Vogel"},
{"name":"Jan Vogel"}
]}}
And there you have it: A data service without a controller and without any procedural logic. From here on, it's just a matter of declaring what queries you'll accept and what each query could return.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.