https://github.com/toreaurstadboss/AspNetCore-GraphQLDemo
The demo repository shows a list of the tallest mountains in the municipialites in Norway. Norway is a land of mountains and it is always to know which mountain is the very tallest in the municipiality you are visiting! (I enjoy mountain climbing and hiking now and then in my spare time).
The demo page shows a text area where you can customize the data to load here. Of course we can only load the data provided for us.
We can also use the Ui playground for GraphQL added for us here too:
First off, we need to grab some Nuget packages for GraphQL. We will be using Asp.Net Core 3.1. in this article.
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.4.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.4.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.5.0-alpha0046" />
Then we need to specify in our Startup class the needed setup.
In ConfigureServices method above we register the schema for our GraphQL method.Startup.cs
using AspNetCore_GraphQLDemo.GraphQL; using AspNetCore_GraphQLDemo.GraphQL.Messaging; using Data; using Data.Repositories; using GraphQL; using GraphQL.Server; using GraphQL.Server.Ui.Playground; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebSockets; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; namespace AspNetCore_GraphQLDemo { public class Startup { private readonly IWebHostEnvironment _env; public Startup(IConfiguration configuration, IWebHostEnvironment env) { _env = env; Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // If using IIS: services.Configure<IISServerOptions>(options => { options.AllowSynchronousIO = true; }); services.AddControllersWithViews(); services.AddHttpContextAccessor(); services.AddRazorPages().AddRazorRuntimeCompilation(); services.AddDbContext<MountainDbContext>(options => { options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); }); services.AddScoped<IMountainRepository, MountainRepository>(); services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService)); services.AddScoped<MountainSchema>(); services.AddSingleton<MountainMessageService>(); services.AddSingleton<MountainDetailsDisplayedMessageService>(); services.AddGraphQL(x => { x.EnableMetrics = true; x.ExposeExceptions = _env.IsDevelopment(); x.SetFieldMiddleware = true; }).AddGraphTypes(ServiceLifetime.Scoped) .AddUserContextBuilder(httpContext => httpContext.User) .AddDataLoader() .AddWebSockets(); services.AddCors(options => { options.AddPolicy(name: "MyAllowSpecificOrigins", builder => { builder.AllowAnyOrigin().AllowAnyMethod(); }); }); } //static IEnumerable<Type> GetGraphQlTypes() //{ // return typeof(Startup).Assembly // .GetTypes() // .Where(x => !x.IsAbstract && // (typeof(IObjectGraphType).IsAssignableFrom(x) || // typeof(IInputObjectGraphType).IsAssignableFrom(x))); //} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } app.UseExceptionHandler(errorApp => { errorApp.Run(async context => { context.Response.Redirect("/Error"); context.Response.StatusCode = 500; var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var exception = exceptionHandlerPathFeature.Error; var result = JsonConvert.SerializeObject(new { error = exception.Message }); context.Response.ContentType = "application/json"; await context.Response.WriteAsync(result); }); }); app.UseStaticFiles(); app.UseRouting(); app.UseCors("MyAllowSpecificOrigins"); app.UseWebSockets(); app.UseGraphQLWebSockets<MountainSchema>("/graphql"); //app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); }); app.UseGraphQL<MountainSchema>(); if (env.IsDevelopment()) { app.UseGraphQLPlayground(new GraphQLPlaygroundOptions { }); } } } }
services.AddScoped<MountainSchema>();
We also add GraphQL itself and setup also web sockets (which are needed for GraphQL).
services.AddGraphQL(x =>
{
x.EnableMetrics = true; x.ExposeExceptions = _env.IsDevelopment(); x.SetFieldMiddleware = true; }).AddGraphTypes(ServiceLifetime.Scoped)
.AddUserContextBuilder(httpContext => httpContext.User)
.AddDataLoader()
.AddWebSockets();
Just as a side note, you want to add Cors also:
services.AddCors(options =>
{
options.AddPolicy(name: "MyAllowSpecificOrigins",
builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod();
});
});
Inside Configure method we add also the following to enable GraphQL:
app.UseCors("MyAllowSpecificOrigins");
app.UseWebSockets();
app.UseGraphQLWebSockets<MountainSchema>("/graphql");
//app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.UseGraphQL<MountainSchema>();
if (env.IsDevelopment())
{
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions
{
});
}
}
Our Mountainschema looks like this:
We pass in a IDependencyResolver (dependency!) into the constructor and resolve the classes we desire (we inherit from Schema class). We wire up our schema here to the Query, Mutation and Subscription we desire and register directives. Here is how the Query property is set:MountainSchema.cs
using AspNetCore_GraphQLDemo.GraphQL.Types; using AspNetCore_GraphQLDemo.GraphQL.Types.Directives; using GraphQL; using GraphQL.Instrumentation; using GraphQL.Types; namespace AspNetCore_GraphQLDemo.GraphQL { public class MountainSchema : Schema { public MountainSchema(IDependencyResolver resolver) : base(resolver) { Query = resolver.Resolve<MountainQuery>(); Mutation = resolver.Resolve<MountainMutation>(); Subscription = resolver.Resolve<MountainSubscription>(); RegisterDirective(new LowercaseDirective()); RegisterDirective(new OrderbyDirective()); var builder = new FieldMiddlewareBuilder(); builder.Use<LowercaseFieldsMiddleware>(); builder.ApplyTo(this); builder.Use(next => { return context => { return next(context).ContinueWith(x => { var c = context; var result = x.Result; result = OrderbyQuery.OrderIfNecessary(context, result); return result; }); }; }); builder.ApplyTo(this); //builder.Use<CustomGraphQlExecutor<MountainSchema>>(); //builder.ApplyTo(this); } } }
As you can see, we can define multiple queries. We inherit from ObjectGraphType and pass in a IMountainRepository. This is an interface for your repository, which fetches data via Entity Framework Core and you can then load data into GraphQL from the local database (The DEMO uses Sql Server (SQLEXPRESS)) via EF Core in a simple manner by only providing the repo via dependency injection. We define via the methods Field and FieldAsync our methods (note the use of string constants as a string value we can use in GraphQL queries of ours that resides in the Schema) and the resolve lambda tells how data is to be fetched. We can specify arguments also. The "mountain" FieldAsync method also accepts arguments via theMountainQuery.cs
using AspNetCore_GraphQLDemo.GraphQL.Types; using Data; using Data.Repositories; using GraphQL.Types; namespace AspNetCore_GraphQLDemo.GraphQL { public class MountainQuery : ObjectGraphType { public MountainQuery(IMountainRepository mountainRepository) { Field<ListGraphType<MountainType>>("mountains", resolve: context => mountainRepository.GetAll() ); FieldAsync<MountainType>("mountain", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<MountainIdInputType>> {Name = "id"}), resolve: async context => { var mountain = context.GetArgument<MountainInfo>("id"); var mountainFromDb = await mountainRepository.GetById(mountain.Id); return mountainFromDb; }); //FieldAsync<MountainType>("selectmountain", // arguments: new QueryArguments(new QueryArgument(typeof(int)) { Name = "id" }), // resolve: async context => // { // var mountain = context.GetArgument<MountainInfo>("id"); // var mountainFromDb = await mountainRepository.GetById(mountain.Id); // return mountainFromDb; // }); //sadly, we need to inherit from IGraphType and cannot just have simple scalar arguments in GraphQL.Net.. } } }
arguments lambda and this allows us parameterized access to our data. Over to the Subscription property. It looks like this:
using AspNetCore_GraphQLDemo.GraphQL.Messaging;
using AspNetCore_GraphQLDemo.GraphQL.Types;
using GraphQL.Resolvers;
using GraphQL.Types;
namespace AspNetCore_GraphQLDemo.GraphQL
{
public class MountainSubscription : ObjectGraphType
{
public MountainSubscription(MountainDetailsDisplayedMessageService mountainDetailsDisplayedMessageService)
{
Name = "Subscription";
AddField(new EventStreamFieldType
{
Name = "detailsDisplayed",
Type = typeof(MountainDetailsMessageType),
Resolver = new FuncFieldResolver<MountainDetailsMessage>(c => c.Source as MountainDetailsMessage),
Subscriber = new EventStreamResolver<MountainDetailsMessage>(c => mountainDetailsDisplayedMessageService.GetMessages())
});
}
}
}
Here we inherit from ObjectGraphType (as we did for Query) and we use the MountainDetailsDisplayedMessageService. This was added as a (concrete class) singleton in the Startup.cs file.
The message service uses RxJs serverside to handle the Pub-sub pattern of the subscriber. We are using System.Reactive.Subjects here.
The mutation looks like this:MountainSubscription.cs
using System; using System.Reactive.Linq; using System.Reactive.Subjects; namespace AspNetCore_GraphQLDemo.GraphQL.Messaging { public class MountainDetailsDisplayedMessageService { private readonly ISubject<MountainDetailsMessage> _messageStream = new ReplaySubject<MountainDetailsMessage>(1); public MountainDetailsMessage AddMountainDetailsMessage(int id) { var message = new MountainDetailsMessage { Id = id }; _messageStream.OnNext(message); return message; } public IObservable<MountainDetailsMessage> GetMessages() { return _messageStream.AsObservable(); } } }
We can create a mountain like this in GraphQL Query:MountainMutation.cs
using AspNetCore_GraphQLDemo.GraphQL.Messaging; using AspNetCore_GraphQLDemo.GraphQL.Types; using Data; using Data.Repositories; using GraphQL.Types; namespace AspNetCore_GraphQLDemo.GraphQL { public class MountainMutation : ObjectGraphType { public MountainMutation(IMountainRepository mountainRepository, MountainMessageService mountainMessageService) { FieldAsync<MountainType>("createMountain", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<MountainInputType>> {Name = "mountain"}), resolve: async context => { var mountain = context.GetArgument<MountainInfo>("mountain"); await mountainRepository.AddMountain(mountain); mountainMessageService.AddMountainAddedMessage(mountain); return mountain; }); FieldAsync<MountainType>("removeMountain", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<MountainIdInputType>> { Name = "id" }), resolve: async context => { var mountain = context.GetArgument<MountainInfo>("id"); await mountainRepository.RemoveMountain(mountain.Id); return mountain; }); } } }
mutation {
createMountain(mountain: {
county: "Svalbard"
muncipiality: "Svalbard"
officialName: "Newtontoppen"
referencePoint: "Isbjønn på toppen"
comments: "Husk rask snøskuter",
metresAboveSeaLevel: "1713",
primaryFactor: "1713"
}) {
id
}
}
And we can remove a mountain (don't we all?) like this:
# Write your query or mutation here
mutation {
removeMountain(id: {
id: 370
}) { id }
}
If you clone the repo you will find more source code concerning directives such as lowercase and sorting. As you saw in MountainSchema I use the FieldMiddlewareBuilder to do the sorting as this needs to tap into the pipeline more of GraphQL.Net.
We also need some more code - for the client side of course.
The client side code relies on Apollo Client lib like this:
The libman.json file (the similar file to package.json when it comes to specifying client-side libraries in .net core mvc solutions) of the demo solution looks like this I have used looks like this:index.cshtml
<script src="https://unpkg.com/apollo-client-browser@1.7.0"></script>
We then need some client side code to load data from GraphQL server of ours.libman.json
{ "version": "1.0", "defaultProvider": "cdnjs", "libraries": [ { "library": "twitter-bootstrap@4.2.1", "destination": "wwwroot/lib/bootstrap", "files": [ "js/bootstrap.bundle.js", "css/bootstrap.min.css" ] }, { "library": "jquery@3.3.1", "destination": "wwwroot/lib/jquery", "files": [ "jquery.min.js" ] }, { "provider": "unpkg", "library": "font-awesome@4.7.0", "destination": "wwwroot/lib/font-awesome/" }, { "provider": "unpkg", "library": "toastr@2.1.4", "destination": "wwwroot/lib/toastr/" } ] }
<script>
function LoadGraphQLDataIntoUi(result) {
var tableBody = $("#mountainsTableBody");
tableBody.empty();
var tableHeaderRow = $("#mountainsTableHeaderRow");
tableHeaderRow.empty();
var rowIndex = 0;
result.data.mountains.forEach(mountain => {
if (rowIndex == 0) {
Object.keys(mountain).forEach(key => {
if (key === '__typename') {
return;
}
tableHeaderRow.append(`<th>${key}</th>`);
});;
}
tableBody.append('<tr>');
Object.keys(mountain).forEach(key => {
if (key === '__typename') {
return;
}
if (key === 'id') {
tableBody.append(`<td><a href='/home/mountaindetails/?id=${mountain[key]}'><i class='fa fa-arrow-right'></i></a> ${mountain[key]}</td>`);
return;
}
tableBody.append(`<td>${mountain[key]}</td>`);
});
tableBody.append('</tr>');
rowIndex++;
});
toastr.success('Loaded GraphQL data from server into the UI successfully.');
}
$("#btnConnect").click(function () {
ConnectDemo();
});
$("#btnLoadData").click(function () {
var gqlQueryContents = $("#GraphQLQuery").val();
LoadGraphQLData(gqlQueryContents, LoadGraphQLDataIntoUi);
toastr.info('Retrieving data from API using GraphQL.');
});
$(document).ready(function () {
console.log('loading');
var initialQuery = `
{
mountains {
id
fylke: county
kommune: muncipiality
hoydeOverHavet: calculatedMetresAboveSeaLevel
offisieltNavn: officialName
primaerfaktor: calculatedPrimaryFactor
referansePunkt: referencePoint
}
}`;
$("#GraphQLQuery").val(initialQuery);
});
</script>
And then a method using Apollo client lib to load the data:
/**
* Loads GraphQL data specified by query expression and passes the 'result' array to the callBackFunction
* callBackFunction should be Js method (function) that accept one parameter, preferably called result, which is an object
* that contains a result.data object.
*/
function LoadGraphQLData(gqlQuery, callBackFunction) {
var apolloClient = new Apollo.lib.ApolloClient({
networkInterface: Apollo.lib.createNetworkInterface({
uri: 'http://localhost:2542/graphql',
transportBatching: true,
}), connectToDevTools: true
});
var query = Apollo.gql(gqlQuery);
apolloClient.query({
query: query,
variables: {}
}).then(result => {
callBackFunction(result);
}).catch(error => {
//debugger
toastr.error(error, 'GraphQL loading failed');
});
}