We will create a GraphQL api using dotnet core 3.1 webapi. For someone from dotnet framework and C# background, dotnet core is awesome due to it's platform independence. You can develop not only on Windows, but also on MacOS or Linux. This leads the way to create a Docker image of your app and host it on Kubernetes as a container orchestrator.
Complete github repo is here
In this article, we'll setup our project structure and create a development environment. Here are the steps;
In a second article, we'll be creating our own Docker image and deploy and run it on OpenShift 4.4 platform as our Kubernetes orchestrator.
Lets start...
We'll be using Visual Studio 2019 as our IDE. Also we need Docker Desktop installed on our development machine. Now we need to check the installed dotnet core SDKs;
D:\>dotnet --list-sdks
2.2.103 [C:\Program Files\dotnet\sdk]
2.2.105 [C:\Program Files\dotnet\sdk]
3.0.101 [C:\Program Files\dotnet\sdk]
3.1.201 [C:\Program Files\dotnet\sdk]
3.1.302 [C:\Program Files\dotnet\sdk]
You can have different SDK versions installed. What we need is SDK 3.1. Now create a folder (mine is graphql-net-core-api-starter
) and navigate there. Issue below cli commands to create necessary projects;
dotnet new webapi --name GraphQL.WebApi
dotnet new sln --name graphql-net-core-api-starter
Eventually, our solution structure should be like this;
Make sure that the project file has <TargetFramework>netcoreapp3.1</TargetFramework>
our launch settings is like below;
graphql.webapi\Properties\launchSettings.json
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"GraphQL.WebApi": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "ui/playground",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Our project's nuget package references are as follows,
<ItemGroup>
<PackageReference Include="graphiql" Version="2.0.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.4.0" />
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.4.0" />
<PackageReference Include="GraphQL.Client.Abstractions.Websocket" Version="3.1.3" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.6" />
<PackageReference Include="System.Reactive" Version="4.4.1" />
<PackageReference Include="System.Reactive.Compatibility" Version="4.4.1" />
</ItemGroup>
navigate to graphql.webapi
and issue dotnet run
you must have a running api on port 5000 now!
Let's add below docker-compose.yaml
file;
version: "3.3"
networks:
graph-starter:
services:
postgresql:
restart: always
image: postgres:12.2-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpassword
- POSTGRES_DB=graphdb
volumes:
- /var/lib/postgresql/data
networks:
- graph-starter
In vscode terminal run the command docker-compose up -d
After pulling necessary packages from docker hub, our db will be ready. Check it out with the command docker ps -a
Its result is supposed to be like;
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
35ba1091da13 postgres:12.2-alpine "docker-entrypoint.s…" 8 minutes ago Up 8 minutes 0.0.0.0:5432->5432/tcp graphql-net-core-api-starter_postgresql_1
We have a postgresql db without even installing it, wonderful!
As models increases, repository methods have to be implemented in each repository. Since most of the implementations will be similar and repetitive, a rather generic approach is necessary. To achieve this goal, we will use generic repository pattern. Thereby all models will inherit existing methods and new ones can be added quickly.
As of dotnet core 3.0, you need to install dotnet-ef as separate tool;
dotnet tool install --global dotnet-ef --version 3.1.6
Now we'll create a base entity from which all the other domain models will be extended. We're assuming that all the models will have id
as primary key and creation_date
fields in database tables.
GraphQL.WebApi\Interfaces\IEntity.cs
using System;
namespace GraphQL.WebApi.Interfaces
{
public interface IEntity
{
int id { get; set; }
DateTime? creation_date { get; set; }
}
}
GraphQL.WebApi\Models\BaseEntity.cs
using GraphQL.WebApi.Interfaces;
using System;
namespace GraphQL.WebApi.Models
{
public abstract class BaseEntity : IEntity
{
public int id { get; set; }
public DateTime? creation_date { get; set; }
}
}
We can create our generic repository;
GraphQL.WebApi\Interfaces\IGenericRepository.cs
using GraphQL.WebApi.Models;
using System.Collections.Generic;
namespace GraphQL.WebApi.Interfaces
{
public interface IGenericRepository<T> where T : BaseEntity
{
IEnumerable<T> GetAll();
T GetById(int id);
T Insert(T entity);
T Update(T entity);
void Delete(int id);
}
}
GraphQL.WebApi\Repositories\GenericRepository.cs
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GraphQL.WebApi.Repository
{
public class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
{
protected readonly DatabaseContext context;
private DbSet<T> entities;
string errorMessage = string.Empty;
public GenericRepository(DatabaseContext context)
{
this.context = context;
entities = context.Set<T>();
}
public IEnumerable<T> GetAll()
{
return entities.AsEnumerable();
}
public T GetById(int id)
{
return entities.SingleOrDefault(s => s.id == id);
}
public T Insert(T entity)
{
if (entity == null) throw new ArgumentNullException("entity");
entities.Add(entity);
context.SaveChanges();
return entity;
}
public T Update(T entity)
{
if (entity == null) throw new ArgumentNullException("entity");
context.SaveChanges();
return entity;
}
public void Delete(int id)
{
T entity = entities.SingleOrDefault(s => s.id == id);
entities.Remove(entity);
context.SaveChanges();
}
}
}
We can now define the database context;
GraphQL.WebApi\Repositories\DatabaseContext.cs
using GraphQL.WebApi.Models;
using Microsoft.EntityFrameworkCore;
namespace GraphQL.WebApi.Repository
{
public class DatabaseContext : DbContext
{
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<City>(entity =>
{
entity.Property(e => e.creation_date).HasDefaultValueSql("(now())");
});
}
}
}
As the initial domain model City
is added to database context. We'll be adding new domain models here. OnModelCreating
is the correct place to further shape the models, set default values, create indexes etc.
Let's add City
as our first model. Database schema and table name is supplied in table attribute,
GraphQL.WebApi\Models\City.cs
using System.ComponentModel.DataAnnotations.Schema;
namespace GraphQL.WebApi.Models
{
[Table("city", Schema = "business")]
public partial class City: BaseEntity
{
public string name { get; set; }
public int? population { get; set; }
}
}
Basically, we're ready to create our first migration. However, we need a way to supply database connection info to database context in designtime. First add the connection string to application configuraion;
GraphQL.WebApi\appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionString": "Server=localhost;Port=5432;Database=graphdb;Username=postgres;Password=postgres"
}
Then, create database context factory. We first read the db connection string from appsettings.Development.json
. Then supply it to DbContextOptionsBuilder
. Finally, create the DatabaseContext
with it.
GraphQL.WebApi\Repositories\DatabaseContextFactory.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
namespace GraphQL.WebApi.Repository
{
public class DatabaseContextFactory : IDesignTimeDbContextFactory<DatabaseContext>
{
public DatabaseContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
var path = Directory.GetCurrentDirectory();
var configuration = new ConfigurationBuilder()
.SetBasePath(path)
.AddJsonFile($"appsettings.Development.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
var connectionString = configuration["ConnectionString"];
Console.WriteLine($"connectionString:{connectionString}");
optionsBuilder.UseNpgsql(connectionString);
return new DatabaseContext(optionsBuilder.Options);
}
}
}
Let's build the project and create our initial migration,
D:\Dev\GITHUB\graphql-net-core-api-starter\GraphQL.WebApi>dotnet ef migrations add InitialCreate
If everything went alright, you'll have an initial migration like this,
Let's add another model Country
,
GraphQL.WebApi\Models\Country.cs
using System.ComponentModel.DataAnnotations.Schema;
namespace GraphQL.WebApi.Models
{
[Table("country", Schema = "business")]
public partial class Country : BaseEntity
{
public string name { get; set; }
public string continent { get; set; }
}
}
Add this new model to DatabaseContext.cs
modelBuilder.Entity<Country>(entity =>
{
entity.Property(e => e.creation_date).HasDefaultValueSql("(now())");
});
Run the command dotnet ef migrations add AddCountry
to add a new migration which includes necessary db schema update on top of InitialCreate
migration.
Very well. Suddenly realized that we forgot to add foreign key relation between City and Country tables. We need to remove lastly added migration running command dotnet ef migrations remove
This will pops out the last migration and it also disapears from Migrations folder.
We can now make the necessary ammendments to the models and create a brand new migration. Updated City.cs
is like this,
using System.ComponentModel.DataAnnotations.Schema;
namespace GraphQL.WebApi.Models
{
[Table("city", Schema = "business")]
public partial class City: BaseEntity
{
public string name { get; set; }
public int? population { get; set; }
public int country_id { get; set; }
[ForeignKey("country_id")]
public Country Country { get; set; }
}
}
Note the ForeignKey
attribute. It'll add county_id
column to city
table and create a foreign key relation with primary key id
column of country
table.
Let's reflect all what we've done so far to database. Issue the command dotnet ef database update
If you're lucky enough, your db is supposed to be updated with these new domain models.
As seen here, graphql db updated with city
and country
tables. I'm using dbeaver for db operations. A fantastic free tool. Highly recommend it.
Finally, we need to update Startup.cs
like below to add the DatabaseContext
to services collection and register GenericRepository
to dependency injection.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(options =>
{
options.UseNpgsql(Configuration["ConnectionString"]);
});
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddControllers();
}
If you'd like to have all the migrations executed on your db when the service starts, add below part to Startup.cs
Configure
if (env.IsDevelopment())
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<DatabaseContext>().Database.Migrate();
}
}
Try dotnet run
Your webapi is still supposed to run on port 5000.
We'll create our GraphQL schema that consist Query, Mutation & Subscription. We'll add pretty many stuff here. After adding necessary folders, project structure is as follows,
Let's start adding our MainMutation
GraphQL.WebApi\Graph\Mutation\MainMutation.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Mutation
{
public class MainMutation : ObjectGraphType
{
public MainMutation(IServiceProvider provider, IWebHostEnvironment env, IFieldService fieldService)
{
Name = "MainMutation";
fieldService.ActivateFields(this, FieldServiceType.Mutation, env, provider);
}
}
}
MainMutation
is a top level entity (like MainQuery
and MainSubscription
) to be added to GraphQL schema. Normally it includes all mutation definitions in it. i.e., you may have tens of mutations defined here. For a trivial project, that would be quiet alright. However, as the project gets larger and the number of mutations & queries increase, this becomes a problem. Let's say you have a large developers team. Everybody will be updating the same file when they update a mutation. The same is valid for query development. This would lead to merge conflicts when developments are to push to GIT. It's not actually very good in terms of transparency. You'll not be able to see all the mutations and queries at a glance. It's much leaner to create separate files for each schema items. This brings a question, how to load these schema items? Answer is IFieldService
GraphQL.WebApi\Interfaces\IFieldService.cs
using GraphQL.Types;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Interfaces
{
public interface IFieldService
{
void ActivateFields(
ObjectGraphType objectGraph,
FieldServiceType fieldType,
IWebHostEnvironment env,
IServiceProvider provider);
void RegisterFields();
}
public enum FieldServiceType
{
Query,
Mutation,
Subscription
}
}
GraphQL.WebApi\Services\FieldService.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
namespace GraphQL.WebApi.Services
{
public class FieldService : IFieldService
{
private IDictionary<FieldServiceType, IList<IFieldServiceItem>> _fieldTable;
private readonly ISubscriptionServices _subscriptionServices;
public FieldService(ISubscriptionServices subscriptionServices)
{
_subscriptionServices = subscriptionServices;
_fieldTable = new Dictionary<FieldServiceType, IList<IFieldServiceItem>>();
_fieldTable.Add(FieldServiceType.Mutation, new List<IFieldServiceItem>());
_fieldTable.Add(FieldServiceType.Query, new List<IFieldServiceItem>());
_fieldTable.Add(FieldServiceType.Subscription, new List<IFieldServiceItem>());
}
public void ActivateFields(
ObjectGraphType objectGraph,
FieldServiceType fieldType,
IWebHostEnvironment env,
IServiceProvider provider)
{
var serviceItemList = _fieldTable[fieldType];
foreach (var serviceItem in serviceItemList)
{
serviceItem.Activate(objectGraph, env, provider);
}
}
public void RegisterFields()
{
var type = typeof(IFieldServiceItem);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
foreach (var fieldType in types)
{
if (fieldType.IsClass)
{
if (typeof(IFieldMutationServiceItem).IsAssignableFrom(fieldType))
{
_fieldTable[FieldServiceType.Mutation].Add((IFieldServiceItem)Activator.CreateInstance(fieldType));
}
else if (typeof(IFieldQueryServiceItem).IsAssignableFrom(fieldType))
{
_fieldTable[FieldServiceType.Query].Add((IFieldServiceItem)Activator.CreateInstance(fieldType));
}
else if (typeof(IFieldSubscriptionServiceItem).IsAssignableFrom(fieldType))
{
_fieldTable[FieldServiceType.Subscription].Add((IFieldServiceItem)Activator.CreateInstance(fieldType));
}
}
}
}
}
}
Now we register this FieldService
in Startup.cs
services.AddScoped<IFieldService, FieldService>();
Let's see our GraphQLSchema
GraphQL.WebApi\Graph\Schema\GraphQLSchema.cs
using GraphQL.WebApi.Graph.Mutation;
using GraphQL.WebApi.Graph.Query;
using GraphQL.WebApi.Graph.Subscription;
using GraphQL.WebApi.Interfaces;
namespace GraphQL.WebApi.Graph.Schema
{
public class GraphQLSchema : GraphQL.Types.Schema
{
public GraphQLSchema(IDependencyResolver resolver) : base(resolver)
{
var fieldService = resolver.Resolve<IFieldService>();
fieldService.RegisterFields();
Mutation = resolver.Resolve<MainMutation>();
Query = resolver.Resolve<MainQuery>();
Subscription = resolver.Resolve<MainSubscription>();
}
}
}
In the constructor, first thing to do is to resolve IFieldService
and call its RegisterFields()
method. What this does is, to collect all the classes which implements IFieldQueryServiceItem
and keep them in _fieldTable
of the FieldService
Then MainMutation
, MainQuery
and MainSubscription
are resolved.
Let's see MainQuery & MainSubscription as well,
GraphQL.WebApi\Graph\Query\MainQuery.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Query
{
public class MainQuery : ObjectGraphType
{
public MainQuery(IServiceProvider provider, IWebHostEnvironment env, IFieldService fieldService)
{
Name = "MainQuery";
fieldService.ActivateFields(this, FieldServiceType.Query, env, provider);
}
}
}
GraphQL.WebApi\Graph\Subscription
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace GraphQL.WebApi.Graph.Subscription
{
public class MainSubscription : ObjectGraphType
{
public MainSubscription(IServiceProvider provider, IWebHostEnvironment env, IFieldService fieldService)
{
Name = "MainSubscription";
fieldService.ActivateFields(this, FieldServiceType.Subscription, env, provider);
}
}
}
Now we need to register them in Startup.cs
services.AddScoped<MainMutation>();
services.AddScoped<MainQuery>();
Let's go over the mutations. They all implement IFieldMutationServiceItem
and has to implement its Activate
method. This leads to standardization and very clean architecture. Our first mutation is addCountry
and it expects a mandadory parameter countryName
. Each query, mutation & subscrition has its own resolver where they carry out the business logic. addCountry
mutation firts receives the parameters and simply inserts new country in the resolver.
GraphQL.WebApi\Graph\Mutation\AddCountryMutation.cs
using GraphQL.Types;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Mutation
{ public class AddCountryMutation : IFieldMutationServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<CountryGType>("addCountry",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "countryName" }
),
resolve: context =>
{
var countryName = context.GetArgument<string>("countryName");
var countryRepository = (IGenericRepository<Country>)sp.GetService(typeof(IGenericRepository<Country>));
var newCountry = new Country
{
name = countryName
};
return countryRepository.Insert(newCountry);
});
}
}
}
addCity
mutation expects countryId
, cityName
mandadory parameters and an optional parameter population
. After obtaining the parameters it creates a new city. It also notifies CityAddedService
which we'll cover shortly.
GraphQL.WebApi\Graph\Mutation\AddCityMutation.cs
using GraphQL.Types;
using GraphQL.WebApi.Dto;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Mutation
{
public class AddCityMutation : IFieldMutationServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<CityGType>("addCity",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "countryId" },
new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "cityName" },
new QueryArgument<IntGraphType> { Name = "population" }
),
resolve: context =>
{
var countryId = context.GetArgument<int>("countryId");
var cityName = context.GetArgument<string>("cityName");
var population = context.GetArgument<int?>("population");
var subscriptionServices = (ISubscriptionServices)sp.GetService(typeof(ISubscriptionServices));
var cityRepository = (IGenericRepository<City>)sp.GetService(typeof(IGenericRepository<City>));
var countryRepository = (IGenericRepository<Country>)sp.GetService(typeof(IGenericRepository<Country>));
var foundCountry = countryRepository.GetById(countryId);
var newCity = new City
{
name = cityName,
country_id = countryId,
population=population
};
var addedCity = cityRepository.Insert(newCity);
subscriptionServices.CityAddedService.AddCityAddedMessage(new CityAddedMessage
{
cityName = addedCity.name,
countryName = foundCountry.name,
id = addedCity.id,
message = "A new city added"
});
return addedCity;
});
}
}
}
addCountry
and addCity
mutations return CountryGType
and CityGType
respectively. Let's go over them,
GraphQL.WebApi\Graph\Type\CityGType.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using System;
namespace GraphQL.WebApi.Graph.Type
{
public class CityGType : ObjectGraphType<City>
{
public IServiceProvider Provider { get; set; }
public CityGType(IServiceProvider provider)
{
Field(x => x.id, type: typeof(IntGraphType));
Field(x => x.name, type: typeof(StringGraphType));
Field(x => x.population, type: typeof(IntGraphType));
Field<CountryGType>("country", resolve: context => {
IGenericRepository<Country> countryRepository = (IGenericRepository<Country>)provider.GetService(typeof(IGenericRepository<Country>));
return countryRepository.GetById(context.Source.country_id);
});
}
}
}
GraphQL.WebApi\Graph\Type\CountryGType.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using System;
using System.Linq;
namespace GraphQL.WebApi.Graph.Type
{
public class CountryGType : ObjectGraphType<Country>
{
public IServiceProvider Provider { get; set; }
public CountryGType(IServiceProvider provider)
{
Field(x => x.id, type: typeof(IntGraphType));
Field(x => x.name, type: typeof(StringGraphType));
Field<ListGraphType<CityGType>>("cities", resolve: context => {
IGenericRepository<City> cityRepository = (IGenericRepository<City>)provider.GetService(typeof(IGenericRepository<City>));
return cityRepository.GetAll().Where(w=> w.Country.id == context.Source.id);
});
}
}
}
All the types which will be returned from our GraphQL api are supposed to extend ObjectGraphType
If they will have some additional properties that are not originally in the base model, like cities
property in CountryGType
, we need to create a resolver for them. We add them to DI as follows,
services.AddScoped<CityGType>();
services.AddScoped<CountryGType>();
Our other two mutations are deleteCity
and updateCity
GraphQL.WebApi\Graph\Mutation\DeleteCityMutation.cs
using GraphQL.Types;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Mutation
{
public class DeleteCityMutation : IFieldMutationServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<StringGraphType>("deleteCity",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "cityId" }
),
resolve: context =>
{
var cityId = context.GetArgument<int>("cityId");
var cityRepository = (IGenericRepository<City>)sp.GetService(typeof(IGenericRepository<City>));
cityRepository.Delete(cityId);
return $"cityId:{cityId} deleted";
});
}
}
}
deleteCity
is very simple. It's receives cityId
mandadory parameter and deletes the city entity through cityRepository
.
GraphQL.WebApi\Graph\Mutation\UpdateCityMutation.cs
using GraphQL.Types;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Mutation
{
public class UpdateCityMutation : IFieldMutationServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<CityGType>("updateCity",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IntGraphType>> { Name = "cityId" },
new QueryArgument<IntGraphType> { Name = "countryId" },
new QueryArgument<StringGraphType> { Name = "cityName" },
new QueryArgument<IntGraphType> { Name = "population" }
),
resolve: context =>
{
var cityId = context.GetArgument<int>("cityId");
var countryId = context.GetArgument<int?>("countryId");
var cityName = context.GetArgument<string>("cityName");
var population = context.GetArgument<int?>("population");
var cityRepository = (IGenericRepository<City>)sp.GetService(typeof(IGenericRepository<City>));
var foundCity = cityRepository.GetById(cityId);
if (countryId != null)
{
foundCity.country_id = countryId.Value;
}
if (!String.IsNullOrEmpty(cityName))
{
foundCity.name = cityName;
}
if (population != null)
{
foundCity.population = population.Value;
}
return cityRepository.Update(foundCity);
});
}
}
}
updateCity
expects a mandatory cityId
of the entity to be updated and three optional parameters. We can update any of the countryId
,cityName
& population
properties.
Let's see our two queries,
GraphQL.WebApi\Graph\Query\Business\CityQuery.cs
using GraphQL.Types;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Linq;
namespace GraphQL.WebApi.Graph.Query.Business
{
public class CityQuery : IFieldQueryServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<ListGraphType<CityGType>>("cities",
arguments: new QueryArguments(
new QueryArgument<StringGraphType> { Name = "name" }
),
resolve: context =>
{
var cityRepository = (IGenericRepository<City>)sp.GetService(typeof(IGenericRepository<City>));
var baseQuery = cityRepository.GetAll();
var name = context.GetArgument<string>("name");
if (name != default(string))
{
return baseQuery.Where(w => w.name.Contains(name));
}
return baseQuery.ToList();
});
}
}
}
GraphQL.WebApi\Graph\Query\Business\CountryQuery.cs
using GraphQL.Types;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using GraphQL.WebApi.Models;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Linq;
namespace GraphQL.WebApi.Graph.Query.Business
{
public class CountryQuery : IFieldQueryServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<ListGraphType<CountryGType>>("countries",
arguments: new QueryArguments(
new QueryArgument<StringGraphType> { Name = "name" }
),
resolve: context =>
{
var countryRepository = (IGenericRepository<Country>)sp.GetService(typeof(IGenericRepository<Country>));
var baseQuery = countryRepository.GetAll();
var name = context.GetArgument<string>("name");
if (name != default(string))
{
return baseQuery.Where(w => w.name.Contains(name));
}
return baseQuery.ToList();
});
}
}
}
Both receives name
parameter and execute related Linq queries.
Sometimes we need to notify the clientside when a certain operation (query / mutation) is carried out on the api. GraphQL Subscription makes use of websocket technology. We already installed
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.4.0" />
nuget package for this purpose.
We configure GraphQL service and add websocket in Configure
like this,
services.AddGraphQL(o => { o.ExposeExceptions = _env.IsDevelopment(); })
.AddGraphTypes(ServiceLifetime.Scoped)
.AddWebSockets();
We've seen that we add Subscription = resolver.Resolve<MainSubscription>();
in our GraphQLSchema.cs
Let's say we'd like to get notified when a new city added to country Germany
We don't need any notification in adding city to other countries.
First we create a subscription SubscriptionServices
and register as singleton in Startup.cs
as
services.AddSingleton<ISubscriptionServices, SubscriptionServices>();
GraphQL.WebApi\Services\SubscriptionServices.cs
using GraphQL.WebApi.Interfaces;
namespace GraphQL.WebApi.Services
{
public class SubscriptionServices : ISubscriptionServices
{
public SubscriptionServices()
{
this.CityAddedService = new CityAddedService();
}
public CityAddedService CityAddedService { get; }
}
}
GraphQL.WebApi\Services\CityAddedService.cs
using GraphQL.WebApi.Dto;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace GraphQL.WebApi.Services
{
public class CityAddedService
{
private readonly ISubject<CityAddedMessage> _messageStream = new ReplaySubject<CityAddedMessage>(1);
public CityAddedMessage AddCityAddedMessage(CityAddedMessage message)
{
_messageStream.OnNext(message);
return message;
}
public IObservable<CityAddedMessage> GetMessages(string countryName)
{
var mess = _messageStream
.Where(message =>
message.countryName == countryName
).Select(s => s)
.AsObservable();
return mess;
}
}
}
CityAddedService
uses System.Reactive.Linq
and System.Reactive.Subjects
. Here we've created a ReplaySubject
which will hold only the last one CityAddedMessage
, without storing the old messages. It's also important to note that GetMessages
method will be called with countryName
parameter as we need to decide if the city has been added to the county we just subscribed. We'll be making use of this GetMessages
method in CityAddedSubscription
GraphQL.WebApi\Graph\Subscription\CityAddedSubscription.cs
using GraphQL.Resolvers;
using GraphQL.Types;
using GraphQL.WebApi.Dto;
using GraphQL.WebApi.Graph.Type;
using GraphQL.WebApi.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System;
namespace GraphQL.WebApi.Graph.Subscription
{
public class CityAddedSubscription : IFieldSubscriptionServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.AddField(new EventStreamFieldType
{
Name = "cityAdded",
Type = typeof(CityAddedMessageGType),
Resolver = new FuncFieldResolver<CityAddedMessage>(context => context.Source as CityAddedMessage),
Arguments = new QueryArguments(
new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "countryName" }
),
Subscriber = new EventStreamResolver<CityAddedMessage>(context =>
{
var subscriptionServices = (ISubscriptionServices)sp.GetService(typeof(ISubscriptionServices));
var countryName = context.GetArgument<string>("countryName");
return subscriptionServices.CityAddedService.GetMessages(countryName);
})
});
}
}
}
As you see, CityAddedSubscription
is another schema object like queries & mutaions. So, it also implements IFieldSubscriptionServiceItem
Interesting part is its Subscriber
resolver. Here we obtain the relevant massage from CityAddedService
using its aforementioned GetMessages
method.
That much talk is enough I guess. Let's try out our api to see it in action. Before that, let me tell you about two wonderful tool which we use in querying our GraphQL api. graphiql and graphql-playground You can use any one of them. We'are making use both to see them in action. Already installed the two necessary nuget packages;
<PackageReference Include="graphiql" Version="2.0.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.4.0" />
We add graphiql by adding below part in ConfigureServices
of Startup.cs
services.AddGraphiQl(x =>
{
x.GraphiQlPath = "/graphiql-ui";
x.GraphQlApiPath = "/graphql";
});
and making use of it like app.UseGraphiQl();
in Configure
For graphql-playground, we add app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
in Configure
One final note, we need to prevent circular referencing error by adding
services.AddControllers()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
Let's build the project in VS2019 and hit F5 to run. Your browser will be opened in http://localhost:5000/ui/playground
displaying playground as we set the launchUrl
in launchSettings.json
Open another browser and navigate to http://localhost:5000/graphiql-ui
Now you have graphiql
You're supposed to see the GraphQL schema with all the elements in both tools.
Try
query countries{
countries{
id
name
}
}
You'll see no data as there's not yet any country. Let's add some,
mutation addCountry{
addCountry(countryName:"France"){
id
name
}
}
mutation addCountry{
addCountry(countryName:"Germany"){
id
name
}
}
When we query countries again you'll see France & Germany,
{
"data": {
"countries": [
{
"id": 1,
"name": "France"
},
{
"id": 2,
"name": "Germany"
}
]
}
}
We can add some cities,
mutation addCityToFrance{
addCity(countryId:1,cityName:"Paris",population:123000){
id
name
population
country{
id
name
}
}
}
mutation addCityToGermany{
addCity(countryId:2,cityName:"Köln",population:500000){
id
name
population
country{
id
name
}
}
}
and query cities,
query cities{
cities{
id
name
population
country{
id
name
}
}
}
Our cities so far,
{
"data": {
"cities": [
{
"id": 1,
"name": "Paris",
"population": 123000,
"country": {
"id": 1,
"name": "France"
}
},
{
"id": 2,
"name": "Köln",
"population": 500000,
"country": {
"id": 2,
"name": "Germany"
}
}
]
}
}
You can simply test DeleteCityMutation
and UpdateCityMutation
in a similar way.
Let's test the most funny part, CityAddedSubscription
Restart webapi. You'll navigate to http://localhost:5000/ui/playground
Open another browser and navigate to http://localhost:5000/graphiql-ui/
Use playground to issue
subscription cityAddedToGermany{
cityAdded(countryName:"Germany"){
id
cityName
countryName
message
}
}
Apparently, we've subscribed to get notified when a city added to Germany. Playground enters into listening mode,
Now let's add a city to Germany using graphiql
Once we execute addCityToGermany
we'll be notified in playground like follows,
Now you can try to add city to France. You're supposed not to see any notification in playground this time!
Using subscription in GraphQL saves us from using additional libraries to use websockets. It also simplifies development as we'll be writing our codes in GraphQL ecosystem. GraphQL makes the heavy lifting for us.
In second part of this article, we will
- add SSL support
- create our Docker image
- and deploy it to OpenShift
Happy coding :-)